Skip to content

Commit 3f54877

Browse files
committed
feat: replace sort order for pending update
1 parent 58abe49 commit 3f54877

11 files changed

Lines changed: 534 additions & 0 deletions

File tree

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ set(ICEBERG_SOURCES
7575
transform.cc
7676
transform_function.cc
7777
type.cc
78+
update/replace_sort_order.cc
7879
update/update_properties.cc
7980
util/bucket_util.cc
8081
util/conversions.cc

src/iceberg/expression/term.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ class ICEBERG_EXPORT BoundReference
151151

152152
std::shared_ptr<Type> type() const override { return field_.type(); }
153153

154+
int32_t field_id() const { return field_.field_id(); }
155+
154156
bool MayProduceNull() const override { return field_.optional(); }
155157

156158
bool Equals(const BoundTerm& other) const override;

src/iceberg/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ iceberg_sources = files(
9797
'transform.cc',
9898
'transform_function.cc',
9999
'type.cc',
100+
'update/replace_sort_order.cc',
100101
'update/update_properties.cc',
101102
'util/bucket_util.cc',
102103
'util/conversions.cc',
@@ -196,6 +197,7 @@ install_headers(
196197
'transform.h',
197198
'type_fwd.h',
198199
'type.h',
200+
'update/replace_sort_order.h',
199201
'update/update_properties.h',
200202
],
201203
subdir: 'iceberg',

src/iceberg/table_metadata.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
#include <chrono>
2424
#include <cstdint>
2525
#include <format>
26+
#include <memory>
2627
#include <optional>
2728
#include <ranges>
2829
#include <string>
2930
#include <unordered_map>
31+
#include <vector>
3032

3133
#include <nlohmann/json.hpp>
3234

@@ -651,6 +653,10 @@ TableMetadataBuilder& TableMetadataBuilder::RemoveEncryptionKey(std::string_view
651653
throw IcebergError(std::format("{} not implemented", __FUNCTION__));
652654
}
653655

656+
const std::vector<std::unique_ptr<TableUpdate>>& TableMetadataBuilder::Changes() const {
657+
return impl_->changes;
658+
}
659+
654660
Result<std::unique_ptr<TableMetadata>> TableMetadataBuilder::Build() {
655661
// 1. Check for accumulated errors
656662
ICEBERG_RETURN_UNEXPECTED(CheckErrors());

src/iceberg/table_metadata.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,13 @@ class ICEBERG_EXPORT TableMetadataBuilder : public ErrorCollector {
418418
/// \return A Result containing the constructed TableMetadata or an error
419419
Result<std::unique_ptr<TableMetadata>> Build();
420420

421+
/// \brief Get the list of changes made to the table metadata
422+
///
423+
/// \return A vector of unique pointers to TableUpdates representing the changes
424+
/// \note We perform error checks and validate metadata consistency in Build(), so call
425+
/// Build() before calling Changes().
426+
const std::vector<std::unique_ptr<TableUpdate>>& Changes() const;
427+
421428
/// \brief Destructor
422429
~TableMetadataBuilder() override;
423430

src/iceberg/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ add_iceberg_test(table_test
7171
SOURCES
7272
json_internal_test.cc
7373
metrics_config_test.cc
74+
replace_sort_order_test.cc
7475
schema_json_test.cc
7576
table_test.cc
7677
table_metadata_builder_test.cc

src/iceberg/test/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ iceberg_tests = {
4848
'sources': files(
4949
'json_internal_test.cc',
5050
'metrics_config_test.cc',
51+
'replace_sort_order_test.cc',
5152
'schema_json_test.cc',
5253
'table_metadata_builder_test.cc',
5354
'table_requirement_test.cc',
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/update/replace_sort_order.h"
21+
22+
#include <memory>
23+
#include <string>
24+
#include <vector>
25+
26+
#include <gmock/gmock.h>
27+
#include <gtest/gtest.h>
28+
29+
#include "iceberg/expression/term.h"
30+
#include "iceberg/result.h"
31+
#include "iceberg/schema.h"
32+
#include "iceberg/schema_field.h"
33+
#include "iceberg/sort_field.h"
34+
#include "iceberg/table.h"
35+
#include "iceberg/table_identifier.h"
36+
#include "iceberg/table_metadata.h"
37+
#include "iceberg/test/matchers.h"
38+
#include "iceberg/test/mock_catalog.h"
39+
#include "iceberg/transform.h"
40+
41+
namespace iceberg {
42+
43+
class ReplaceSortOrderTest : public ::testing::Test {
44+
protected:
45+
void SetUp() override {
46+
// Create a simple schema with various field types
47+
SchemaField field1(1, "id", std::make_shared<LongType>(), false);
48+
SchemaField field2(2, "name", std::make_shared<StringType>(), true);
49+
SchemaField field3(3, "ts", std::make_shared<TimestampType>(), true);
50+
SchemaField field4(4, "price", std::make_shared<DecimalType>(10, 2), true);
51+
schema_ = std::make_shared<Schema>(
52+
std::vector<SchemaField>{field1, field2, field3, field4}, 1);
53+
54+
// Create basic table metadata
55+
metadata_ = std::make_shared<TableMetadata>();
56+
metadata_->schemas.push_back(schema_);
57+
metadata_->current_schema_id = 1;
58+
59+
// Create catalog and table identifier
60+
catalog_ = std::make_shared<MockCatalog>();
61+
identifier_ = TableIdentifier(Namespace({"test"}), "table");
62+
}
63+
64+
std::shared_ptr<Schema> schema_;
65+
std::shared_ptr<TableMetadata> metadata_;
66+
std::shared_ptr<MockCatalog> catalog_;
67+
TableIdentifier identifier_;
68+
};
69+
70+
TEST_F(ReplaceSortOrderTest, AscendingSingleField) {
71+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
72+
73+
auto ref = NamedReference::Make("id").value();
74+
auto term = UnboundTransform::Make(std::move(ref), Transform::Identity()).value();
75+
76+
update.Asc(std::move(term), NullOrder::kFirst);
77+
78+
auto result = update.Apply();
79+
EXPECT_THAT(result, IsOk());
80+
81+
auto sort_order = update.GetBuiltSortOrder();
82+
ASSERT_NE(sort_order, nullptr);
83+
EXPECT_EQ(sort_order->fields().size(), 1);
84+
85+
const auto& field = sort_order->fields()[0];
86+
EXPECT_EQ(field.source_id(), 1);
87+
EXPECT_EQ(field.direction(), SortDirection::kAscending);
88+
EXPECT_EQ(field.null_order(), NullOrder::kFirst);
89+
}
90+
91+
TEST_F(ReplaceSortOrderTest, DescendingSingleField) {
92+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
93+
94+
auto ref = NamedReference::Make("name").value();
95+
auto term = UnboundTransform::Make(std::move(ref), Transform::Identity()).value();
96+
97+
update.Desc(std::move(term), NullOrder::kLast);
98+
99+
auto result = update.Apply();
100+
EXPECT_THAT(result, IsOk());
101+
102+
auto sort_order = update.GetBuiltSortOrder();
103+
ASSERT_NE(sort_order, nullptr);
104+
EXPECT_EQ(sort_order->fields().size(), 1);
105+
106+
const auto& field = sort_order->fields()[0];
107+
EXPECT_EQ(field.source_id(), 2);
108+
EXPECT_EQ(field.direction(), SortDirection::kDescending);
109+
EXPECT_EQ(field.null_order(), NullOrder::kLast);
110+
}
111+
112+
TEST_F(ReplaceSortOrderTest, MultipleFields) {
113+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
114+
115+
auto ref1 = NamedReference::Make("name").value();
116+
auto term1 = UnboundTransform::Make(std::move(ref1), Transform::Identity()).value();
117+
118+
auto ref2 = NamedReference::Make("id").value();
119+
auto term2 = UnboundTransform::Make(std::move(ref2), Transform::Identity()).value();
120+
121+
update.Asc(std::move(term1), NullOrder::kFirst)
122+
.Desc(std::move(term2), NullOrder::kLast);
123+
124+
auto result = update.Apply();
125+
EXPECT_THAT(result, IsOk());
126+
127+
auto sort_order = update.GetBuiltSortOrder();
128+
ASSERT_NE(sort_order, nullptr);
129+
EXPECT_EQ(sort_order->fields().size(), 2);
130+
131+
// Check first field
132+
const auto& field1 = sort_order->fields()[0];
133+
EXPECT_EQ(field1.source_id(), 2); // name field
134+
EXPECT_EQ(field1.direction(), SortDirection::kAscending);
135+
EXPECT_EQ(field1.null_order(), NullOrder::kFirst);
136+
137+
// Check second field
138+
const auto& field2 = sort_order->fields()[1];
139+
EXPECT_EQ(field2.source_id(), 1); // id field
140+
EXPECT_EQ(field2.direction(), SortDirection::kDescending);
141+
EXPECT_EQ(field2.null_order(), NullOrder::kLast);
142+
}
143+
144+
TEST_F(ReplaceSortOrderTest, WithTransform) {
145+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
146+
147+
auto ref = NamedReference::Make("ts").value();
148+
auto term = UnboundTransform::Make(std::move(ref), Transform::Day()).value();
149+
150+
update.Asc(std::move(term), NullOrder::kFirst);
151+
152+
auto result = update.Apply();
153+
EXPECT_THAT(result, IsOk());
154+
155+
auto sort_order = update.GetBuiltSortOrder();
156+
ASSERT_NE(sort_order, nullptr);
157+
EXPECT_EQ(sort_order->fields().size(), 1);
158+
159+
const auto& field = sort_order->fields()[0];
160+
EXPECT_EQ(field.source_id(), 3);
161+
EXPECT_EQ(field.transform()->ToString(), "day");
162+
EXPECT_EQ(field.direction(), SortDirection::kAscending);
163+
EXPECT_EQ(field.null_order(), NullOrder::kFirst);
164+
}
165+
166+
TEST_F(ReplaceSortOrderTest, WithBucketTransform) {
167+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
168+
169+
auto ref = NamedReference::Make("name").value();
170+
auto term = UnboundTransform::Make(std::move(ref), Transform::Bucket(10)).value();
171+
172+
update.Desc(std::move(term), NullOrder::kLast);
173+
174+
auto result = update.Apply();
175+
EXPECT_THAT(result, IsOk());
176+
177+
auto sort_order = update.GetBuiltSortOrder();
178+
ASSERT_NE(sort_order, nullptr);
179+
EXPECT_EQ(sort_order->fields().size(), 1);
180+
181+
const auto& field = sort_order->fields()[0];
182+
EXPECT_EQ(field.source_id(), 2);
183+
EXPECT_EQ(field.transform()->ToString(), "bucket[10]");
184+
EXPECT_EQ(field.direction(), SortDirection::kDescending);
185+
EXPECT_EQ(field.null_order(), NullOrder::kLast);
186+
}
187+
188+
TEST_F(ReplaceSortOrderTest, NullTerm) {
189+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
190+
191+
update.Asc(nullptr, NullOrder::kFirst);
192+
193+
auto result = update.Apply();
194+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
195+
EXPECT_THAT(result, HasErrorMessage("Term cannot be null"));
196+
}
197+
198+
TEST_F(ReplaceSortOrderTest, InvalidTransformForType) {
199+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
200+
201+
// Try to apply day transform to a string field (invalid)
202+
auto ref = NamedReference::Make("name").value();
203+
auto term = UnboundTransform::Make(std::move(ref), Transform::Day()).value();
204+
205+
update.Asc(std::move(term), NullOrder::kFirst);
206+
207+
auto result = update.Apply();
208+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
209+
EXPECT_THAT(result, HasErrorMessage("not a valid input type"));
210+
}
211+
212+
TEST_F(ReplaceSortOrderTest, NonExistentField) {
213+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
214+
215+
auto ref = NamedReference::Make("nonexistent").value();
216+
auto term = UnboundTransform::Make(std::move(ref), Transform::Identity()).value();
217+
218+
update.Asc(std::move(term), NullOrder::kFirst);
219+
220+
auto result = update.Apply();
221+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
222+
EXPECT_THAT(result, HasErrorMessage("Cannot find"));
223+
}
224+
225+
TEST_F(ReplaceSortOrderTest, CaseSensitiveTrue) {
226+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
227+
228+
auto ref = NamedReference::Make("ID").value(); // Uppercase
229+
auto term = UnboundTransform::Make(std::move(ref), Transform::Identity()).value();
230+
231+
update.CaseSensitive(true).Asc(std::move(term), NullOrder::kFirst);
232+
233+
auto result = update.Apply();
234+
// Should fail because schema has "id" (lowercase)
235+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
236+
}
237+
238+
TEST_F(ReplaceSortOrderTest, CaseSensitiveFalse) {
239+
ReplaceSortOrder update(identifier_, catalog_, metadata_);
240+
241+
auto ref = NamedReference::Make("ID").value(); // Uppercase
242+
auto term = UnboundTransform::Make(std::move(ref), Transform::Identity()).value();
243+
244+
update.CaseSensitive(false).Asc(std::move(term), NullOrder::kFirst);
245+
246+
auto result = update.Apply();
247+
// Should succeed because case-insensitive matching
248+
EXPECT_THAT(result, IsOk());
249+
250+
auto sort_order = update.GetBuiltSortOrder();
251+
ASSERT_NE(sort_order, nullptr);
252+
EXPECT_EQ(sort_order->fields().size(), 1);
253+
EXPECT_EQ(sort_order->fields()[0].source_id(), 1);
254+
}
255+
256+
} // namespace iceberg

src/iceberg/type_fwd.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ class PendingUpdate;
163163
template <typename T>
164164
class PendingUpdateTyped;
165165
class UpdateProperties;
166+
class ReplaceSortOrder;
166167

167168
/// ----------------------------------------------------------------------------
168169
/// TODO: Forward declarations below are not added yet.

0 commit comments

Comments
 (0)