Skip to content

Commit 6c30d3c

Browse files
fix: validate transform parameters in metadata (#757)
## Summary - add `Transform::Validate` to reuse bind-time transform checks during metadata validation - reject invalid bucket/truncate parameters when parsing transform strings - make partition specs and sort orders reject non-positive bucket/truncate parameters during schema-bound validation ## Validation - `cmake -S . -B build-transform-params -G Ninja -DICEBERG_BUILD_BUNDLE=OFF -DICEBERG_BUILD_REST=OFF -DICEBERG_BUILD_SHARED=OFF` - `ctest --test-dir build-transform-params -R schema_test --output-on-failure`
1 parent 944a544 commit 6c30d3c

9 files changed

Lines changed: 78 additions & 15 deletions

File tree

src/iceberg/partition_spec.cc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,7 @@ Status PartitionSpec::Validate(const Schema& schema, bool allow_missing_fields)
192192
partition_field);
193193
}
194194
const auto& source_type = source_field.value().get().type();
195-
if (!field_transform->CanTransform(*source_type)) {
196-
return InvalidArgument("Invalid source type {} for transform {}",
197-
source_type->ToString(), field_transform->ToString());
198-
}
195+
ICEBERG_RETURN_UNEXPECTED(field_transform->Validate(source_type));
199196

200197
// The only valid parent types for a PartitionField are StructTypes. This must be
201198
// checked recursively.

src/iceberg/sort_order.cc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,7 @@ Status SortOrder::Validate(const Schema& schema) const {
9696

9797
const auto& source_type = schema_field.value().get().type();
9898

99-
if (!field.transform()->CanTransform(*source_type)) {
100-
return InvalidArgument("Invalid source type {} for transform {}",
101-
source_type->ToString(), field.transform()->ToString());
102-
}
99+
ICEBERG_RETURN_UNEXPECTED(field.transform()->Validate(source_type));
103100
}
104101
return {};
105102
}

src/iceberg/test/partition_spec_test.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,22 @@ TEST(PartitionSpecTest, InvalidTransformForType) {
219219
EXPECT_THAT(result_void, IsOk());
220220
}
221221

222+
TEST(PartitionSpecTest, InvalidParameterizedTransform) {
223+
auto field_id = SchemaField::MakeRequired(1, "id", int32());
224+
auto field_name = SchemaField::MakeRequired(2, "name", string());
225+
Schema schema({field_id, field_name}, Schema::kInitialSchemaId);
226+
227+
PartitionField bucket_field(1, 1000, "id_bucket", Transform::Bucket(0));
228+
auto bucket_result = PartitionSpec::Make(schema, 1, {bucket_field}, false);
229+
EXPECT_THAT(bucket_result, IsError(ErrorKind::kInvalidArgument));
230+
EXPECT_THAT(bucket_result, HasErrorMessage("Number of buckets must be positive"));
231+
232+
PartitionField truncate_field(2, 1000, "name_trunc", Transform::Truncate(0));
233+
auto truncate_result = PartitionSpec::Make(schema, 1, {truncate_field}, false);
234+
EXPECT_THAT(truncate_result, IsError(ErrorKind::kInvalidArgument));
235+
EXPECT_THAT(truncate_result, HasErrorMessage("Width must be positive"));
236+
}
237+
222238
TEST(PartitionSpecTest, SourceIdNotFound) {
223239
auto field1 = SchemaField::MakeRequired(1, "id", int64());
224240
auto field2 = SchemaField::MakeRequired(2, "ts", timestamp());

src/iceberg/test/sort_order_test.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ TEST_F(SortOrderTest, MakeInvalidSortOrderTransformCannotApply) {
225225
EXPECT_THAT(sort_order, HasErrorMessage("Invalid source type"));
226226
}
227227

228+
TEST_F(SortOrderTest, MakeInvalidParameterizedTransform) {
229+
SortField bucket_field(1, Transform::Bucket(0), SortDirection::kAscending,
230+
NullOrder::kFirst);
231+
auto bucket_order = SortOrder::Make(*schema_, 1, std::vector<SortField>{bucket_field});
232+
EXPECT_THAT(bucket_order, IsError(ErrorKind::kInvalidArgument));
233+
EXPECT_THAT(bucket_order, HasErrorMessage("Number of buckets must be positive"));
234+
235+
SortField truncate_field(2, Transform::Truncate(0), SortDirection::kAscending,
236+
NullOrder::kFirst);
237+
auto truncate_order =
238+
SortOrder::Make(*schema_, 1, std::vector<SortField>{truncate_field});
239+
EXPECT_THAT(truncate_order, IsError(ErrorKind::kInvalidArgument));
240+
EXPECT_THAT(truncate_order, HasErrorMessage("Width must be positive"));
241+
}
242+
228243
TEST_F(SortOrderTest, MakeInvalidSortOrderNonPrimitiveField) {
229244
auto struct_field = std::make_unique<SchemaField>(
230245
4, "struct_field",

src/iceberg/test/transform_test.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,12 @@ TEST(TransformFromStringTest, PositiveCases) {
167167
}
168168

169169
TEST(TransformFromStringTest, NegativeCases) {
170-
constexpr std::array<std::string_view, 6> invalid_cases = {
170+
constexpr std::array<std::string_view, 8> invalid_cases = {
171171
"bucket", // missing param
172172
"bucket[]", // empty param
173173
"bucket[abc]", // invalid number
174+
"bucket[0]", // invalid number of buckets
175+
"truncate[0]", // invalid width
174176
"unknown", // unsupported transform
175177
"bucket[16", // missing closing bracket
176178
"truncate[1]extra" // extra characters

src/iceberg/transform.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,19 @@ bool Transform::CanTransform(const Type& source_type) const {
234234
std::unreachable();
235235
}
236236

237+
Status Transform::Validate(const std::shared_ptr<Type>& source_type) const {
238+
if (!source_type) {
239+
return InvalidArgument("Source type cannot be null for transform {}", ToString());
240+
}
241+
// Keep type mismatches as InvalidArgument; Bind reports them as NotSupported.
242+
if (!CanTransform(*source_type)) {
243+
return InvalidArgument("Invalid source type {} for transform {}",
244+
source_type->ToString(), ToString());
245+
}
246+
ICEBERG_RETURN_UNEXPECTED(Bind(source_type));
247+
return {};
248+
}
249+
237250
bool Transform::PreservesOrder() const {
238251
switch (transform_type_) {
239252
case TransformType::kUnknown:
@@ -556,9 +569,11 @@ Result<std::shared_ptr<Transform>> TransformFromString(std::string_view transfor
556569
StringUtils::ParseNumber<int32_t>(match[2].str()));
557570

558571
if (type_str == kBucketName) {
572+
ICEBERG_RETURN_UNEXPECTED(internal::ValidateBucketTransformParameter(param));
559573
return Transform::Bucket(param);
560574
}
561575
if (type_str == kTruncateName) {
576+
ICEBERG_RETURN_UNEXPECTED(internal::ValidateTruncateTransformParameter(param));
562577
return Transform::Truncate(param);
563578
}
564579
}

src/iceberg/transform.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ class ICEBERG_EXPORT Transform : public util::Formattable {
159159
/// \return true if this transform can be applied to the type, false otherwise
160160
bool CanTransform(const Type& source_type) const;
161161

162+
/// \brief Validates whether this transform can bind to the given source type.
163+
/// \param source_type The source type to validate against.
164+
/// \return Error status if the transform cannot bind to the source type.
165+
Status Validate(const std::shared_ptr<Type>& source_type) const;
166+
162167
/// \brief Whether the transform preserves the order of values (is monotonic).
163168
bool PreservesOrder() const;
164169

src/iceberg/transform_function.cc

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "iceberg/util/bucket_util.h"
2828
#include "iceberg/util/macros.h"
2929
#include "iceberg/util/temporal_util.h"
30+
#include "iceberg/util/transform_util.h"
3031
#include "iceberg/util/truncate_util.h"
3132

3233
namespace iceberg {
@@ -92,9 +93,7 @@ Result<std::unique_ptr<TransformFunction>> BucketTransform::Make(
9293
return NotSupported("{} is not a valid input type for bucket transform",
9394
source_type->ToString());
9495
}
95-
if (num_buckets <= 0) {
96-
return InvalidArgument("Number of buckets must be positive, got {}", num_buckets);
97-
}
96+
ICEBERG_RETURN_UNEXPECTED(internal::ValidateBucketTransformParameter(num_buckets));
9897
return std::make_unique<BucketTransform>(source_type, num_buckets);
9998
}
10099

@@ -126,9 +125,7 @@ Result<std::unique_ptr<TransformFunction>> TruncateTransform::Make(
126125
return NotSupported("{} is not a valid input type for truncate transform",
127126
source_type->ToString());
128127
}
129-
if (width <= 0) {
130-
return InvalidArgument("Width must be positive, got {}", width);
131-
}
128+
ICEBERG_RETURN_UNEXPECTED(internal::ValidateTruncateTransformParameter(width));
132129
return std::make_unique<TruncateTransform>(source_type, width);
133130
}
134131

src/iceberg/util/transform_util.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,25 @@
2626
#include <string>
2727

2828
#include "iceberg/iceberg_export.h"
29+
#include "iceberg/result.h"
30+
31+
namespace iceberg::internal {
32+
33+
inline Status ValidateBucketTransformParameter(int32_t num_buckets) {
34+
if (num_buckets <= 0) {
35+
return InvalidArgument("Number of buckets must be positive, got {}", num_buckets);
36+
}
37+
return {};
38+
}
39+
40+
inline Status ValidateTruncateTransformParameter(int32_t width) {
41+
if (width <= 0) {
42+
return InvalidArgument("Width must be positive, got {}", width);
43+
}
44+
return {};
45+
}
46+
47+
} // namespace iceberg::internal
2948

3049
namespace iceberg {
3150

0 commit comments

Comments
 (0)