This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 2b133e0c9c [#13312] fix(core): Validate name and comment length before
persisting entities (#13322)
2b133e0c9c is described below
commit 2b133e0c9c07cdfc0f65142c25fac9a6e3a1c322
Author: Jerry Shao <[email protected]>
AuthorDate: Fri Sep 18 20:39:19 2026 +0800
[#13312] fix(core): Validate name and comment length before persisting
entities (#13322)
### What changes were proposed in this pull request?
Names, aliases and comments are now checked against their store column
width before they are written. Values that are too long are rejected
with a 400 that names the entity, the field and the limit, e.g. `The
name of the tag must not exceed 128 characters`.
- **Entity layer:** `Field` supports an optional max length, and
`Entity#validate()` passes the entity type into the error message.
Entities are validated when built, which covers both create and alter.
- Name (128): tag, policy, role, user, group, job template.
- Comment (256): metalake, catalog, schema, fileset, topic, tag.
- Alias (128): model version aliases, checked in
`ModelVersionEntity#validate()`.
- The limits are defined in `EntityFieldLimits` and match every schema
script. Length is counted in code points, as MySQL (utf8mb4) and
PostgreSQL do.
- Out of scope: policy and job template comments are `TEXT`/`CLOB`
columns, so they stay unlimited.
- **Early checks** where the catalog creates external resources before
the entity is built:
- fileset and schema create in `FilesetCatalogOperations`, before the
directories are created;
- topic create and comment update in `TopicNormalizeDispatcher`, before
Kafka is called.
- **Model rename:** `ModelNormalizeDispatcher` now applies the name
specification to `ModelChange.rename`, as table, fileset and view
renames already do. Previously a model rename skipped the name check.
- **Fallback:** the SQL exception converters map value-too-long errors
(PostgreSQL and H2 `22001`, MySQL `1406`) to `IllegalArgumentException`.
The SQL exception is logged on the server but not attached as the cause,
because the error response serializes the full stack trace.
### Why are the changes needed?
Values longer than the column reached the database, and the server
returned 500 with the raw database error. For topics, the store failure
was swallowed: the Kafka topic was created but the entity was not
persisted.
Fix: #13312
Remaining gaps are tracked in #13317.
### Does this PR introduce _any_ user-facing change?
Yes.
- Too-long values now return 400 instead of 500, and the error no longer
contains the database message.
- A topic with a too-long comment is rejected before it is created in
Kafka.
- There are two separate rules, so they should not be read as one limit:
- **Store column width.** Tag, policy, role, user, group and job
template names and model version aliases are checked against their
128-character column. These names do not go through the name
specification.
- **Name specification** (`^\w[\w/=-]{0,63}$`, no reserved words).
Renaming a model now follows the same rule as creating one, as table,
fileset and view renames already do: at most 64 characters, only
letters, digits, `_`, `/`, `=` and `-`. New names outside this rule
return 400.
### How was this patch tested?
- **Unit tests:**
- `TestEntityFieldLimits`: every limited field at and over its limit,
plus code-point counting.
- `TestField` and the converter tests.
- `TestModelNormalizeDispatcher` (new): model rename name rules.
- **Real H2 store:**
- Tag create, rename and comment update in `TestTagManager`.
- Fileset and schema create in `TestFilesetCatalogOperations`: no
directory is left behind.
- Fileset comment update in `TestFilesetCatalogOperations`.
- `TestFilesetMetaService`: the rollback test now fails the version
insert on a real H2 column, which also covers the `22001` fallback end
to end.
- **Integration tests:**
- New `NameAndCommentLengthIT`: tag, policy, catalog, schema and
fileset. It needs no Docker, so it runs on every metadata backend.
- `MetalakeIT`, `AccessControlIT`, and `ModelCatalogOperationsIT` (alias
length and model rename).
- `./gradlew :core:test :server:test :catalogs:catalog-fileset:test
:catalogs:catalog-model:test -PskipITs` passes, and the ITs above 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 ++
.../relational/service/TestFilesetMetaService.java | 53 +++--
.../org/apache/gravitino/tag/TestTagManager.java | 45 ++++
37 files changed, 1260 insertions(+), 36 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 5f06f75f25..42b015845e 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
@@ -70,6 +70,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.NameIdentifier;
@@ -452,6 +453,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)) {
@@ -815,6 +821,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 6ca9cc9163..eeee30469d 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
@@ -76,11 +76,13 @@ 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.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;
@@ -495,6 +497,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(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();
@@ -1416,6 +1438,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(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(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,
secretManager)) {
+ 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 8aac74db05..4aee6d73ee 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;
@@ -1259,6 +1260,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 static void assertPropertiesEqual(
Map<String, String> expectedUserProps, Map<String, String> actual) {
Assertions.assertFalse(actual.containsKey(StringIdentifier.ID_KEY));
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 f88fc9f11f..62b0e8acde 100644
--- a/core/src/main/java/org/apache/gravitino/Entity.java
+++ b/core/src/main/java/org/apache/gravitino/Entity.java
@@ -92,7 +92,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 60c0be7754..e6323ab984 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(
@@ -360,6 +373,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) {
applyNameSpecification(Capability.Scope.VIEW, renameView.getNewName(),
capabilities);
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 978680b32b..91e8c5ce1d 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
@@ -154,7 +154,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 323d5ee285..bc7625020c 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
@@ -23,6 +23,8 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.util.Map;
+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;
@@ -72,12 +74,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);
@@ -100,6 +109,11 @@ public class TopicNormalizeDispatcher implements
TopicDispatcher {
return applyCaseSensitive(topicIdent, 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 d306aaacef..ad9bcf2af3 100644
--- a/core/src/main/java/org/apache/gravitino/meta/PolicyEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/PolicyEntity.java
@@ -30,6 +30,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;
@@ -43,7 +44,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 7049a5121e..7343875ec8 100644
--- a/core/src/main/java/org/apache/gravitino/meta/TagEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/TagEntity.java
@@ -29,6 +29,7 @@ import javax.annotation.Nullable;
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,10 +43,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..f9e7b8ce90
--- /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 = "testRenameNameSpec";
+ 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/storage/relational/service/TestFilesetMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestFilesetMetaService.java
index 5255065194..737e998520 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestFilesetMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestFilesetMetaService.java
@@ -727,28 +727,37 @@ public class TestFilesetMetaService extends
TestJDBCBackend {
"/tmp-v1");
FilesetMetaService.getInstance().insertFileset(original, false);
FilesetPO initialPO = getFilesetPO(original.id());
- // fileset_meta carries no comment, so an over-long comment passes the
metadata update and only
- // fails once the version snapshot is written.
- String tooLongComment = StringUtils.repeat("c", 300);
-
- // Each backend reports the rejected snapshot differently, so only the
rollback below is
- // asserted on.
- assertThrows(
- Exception.class,
- () ->
- FilesetMetaService.getInstance()
- .updateFileset(
- original.nameIdentifier(),
- entity -> {
- FilesetEntity current = (FilesetEntity) entity;
- return copyFileset(
- current,
- current.id(),
- current.name(),
- tooLongComment,
- "/tmp-v2",
- current.auditInfo());
- }));
+ // fileset_meta carries no storage location, so an over-long storage
location name passes the
+ // metadata update and only fails once the version snapshot is written.
The comment can't be
+ // used for this since the entity validation rejects an over-long comment
before any write.
+ String tooLongLocationName = StringUtils.repeat("l", 300);
+
+ // The value-too-long error of every backend is converted to an
IllegalArgumentException
+ // without the database error as its cause.
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ FilesetMetaService.getInstance()
+ .updateFileset(
+ original.nameIdentifier(),
+ entity -> {
+ FilesetEntity current = (FilesetEntity) entity;
+ return FilesetEntity.builder()
+ .withId(current.id())
+ .withName(current.name())
+ .withNamespace(current.namespace())
+ .withFilesetType(current.filesetType())
+
.withStorageLocations(ImmutableMap.of(tooLongLocationName, "/tmp-v2"))
+ .withComment("new comment")
+ .withProperties(current.properties())
+ .withAuditInfo(current.auditInfo())
+ .build();
+ }));
+ Assertions.assertEquals(
+ "The fileset entity has a value that exceeds the maximum length of its
column.",
+ exception.getMessage());
+ Assertions.assertNull(exception.getCause());
FilesetEntity current =
FilesetMetaService.getInstance().getFilesetByIdentifier(original.nameIdentifier());
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 1451e2fc95..a0fb0d3a76 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;
@@ -443,6 +445,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);