Skip to content

Commit d479f23

Browse files
manuzhangclaude
andcommitted
fix: align boolean parsing with Iceberg Java
Boolean values were parsed ad hoc in four places, and not consistently: `arrow_s3_file_io.cc` rejected anything that was not "true" or "false", while `config.h`, `auth_managers.cc` and `rest_catalog.cc` read any unrecognized value as false. Each site spelled out the "true"/"false" literals itself, and `StringUtils` covered only numbers. Add `StringUtils::ParseBoolean()`, which mirrors Java's Boolean.parseBoolean, and `PropertyUtil::PropertyAsBoolean()` / `PropertyUtil::PropertyAsOptionalBoolean()`, which mirror Java's PropertyUtil.propertyAsBoolean and propertyAsNullableBoolean, and read every boolean through them. That settles the codebase on Java's contract: "true" ignoring case reads as true, and every other value reads as false. `s3.path-style-access` and `s3.ssl.enabled` change behavior accordingly: a malformed value used to fail the FileIO build and now reads as false, as it does in Java, whose S3FileIOProperties reads both with PropertyUtil.propertyAsBoolean. The test that pinned the old behavior is rewritten to pin the new one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 168cb52 commit d479f23

11 files changed

Lines changed: 157 additions & 28 deletions

File tree

‎src/iceberg/arrow/s3/arrow_s3_file_io.cc‎

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "iceberg/arrow/s3/s3_properties.h"
3838
#include "iceberg/logging/log_macros.h"
3939
#include "iceberg/util/macros.h"
40+
#include "iceberg/util/property_util.h"
4041
#include "iceberg/util/string_util.h"
4142

4243
namespace iceberg::arrow {
@@ -52,22 +53,6 @@ const std::string* FindProperty(
5253
return it == properties.end() ? nullptr : &it->second;
5354
}
5455

55-
Result<std::optional<bool>> ParseOptionalBool(
56-
const std::unordered_map<std::string, std::string>& properties,
57-
std::string_view key) {
58-
const auto* value = FindProperty(properties, key);
59-
if (value == nullptr) {
60-
return std::nullopt;
61-
}
62-
if (StringUtils::EqualsIgnoreCase(*value, "true")) {
63-
return true;
64-
}
65-
if (StringUtils::EqualsIgnoreCase(*value, "false")) {
66-
return false;
67-
}
68-
return InvalidArgument(R"("{}" must be "true" or "false")", key);
69-
}
70-
7156
Status EnsureS3Initialized() {
7257
static const ::arrow::Status init_status = []() {
7358
auto options = ::arrow::fs::S3GlobalOptions::Defaults();
@@ -136,15 +121,15 @@ Result<::arrow::fs::S3Options> ConfigureS3Options(
136121
options.endpoint_override = SplitEndpointScheme(endpoint_env, options);
137122
}
138123

139-
ICEBERG_ASSIGN_OR_RAISE(const auto path_style_access,
140-
ParseOptionalBool(properties, S3Properties::kPathStyleAccess));
124+
const auto path_style_access =
125+
PropertyUtil::PropertyAsOptionalBoolean(properties, S3Properties::kPathStyleAccess);
141126
if (path_style_access.has_value()) {
142127
options.force_virtual_addressing = !*path_style_access;
143128
}
144129

145130
// Explicit `s3.ssl.enabled` overrides any endpoint-derived scheme.
146-
ICEBERG_ASSIGN_OR_RAISE(const auto ssl_enabled,
147-
ParseOptionalBool(properties, S3Properties::kSslEnabled));
131+
const auto ssl_enabled =
132+
PropertyUtil::PropertyAsOptionalBoolean(properties, S3Properties::kSslEnabled);
148133
if (ssl_enabled.has_value()) {
149134
options.scheme = *ssl_enabled ? "https" : "http";
150135
}

‎src/iceberg/catalog/rest/auth/auth_managers.cc‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#include "iceberg/catalog/rest/auth/auth_manager_internal.h"
2525
#include "iceberg/catalog/rest/auth/auth_properties.h"
26+
#include "iceberg/util/property_util.h"
2627
#include "iceberg/util/string_util.h"
2728

2829
namespace iceberg::rest::auth {
@@ -47,8 +48,8 @@ const std::unordered_set<std::string, StringHash, StringEqual>& KnownAuthTypes()
4748
std::string InferAuthType(
4849
const std::unordered_map<std::string, std::string>& properties) {
4950
// Deprecated alias: rest.sigv4-enabled=true forces SigV4.
50-
if (auto it = properties.find(AuthProperties::kSigV4Enabled);
51-
it != properties.end() && StringUtils::EqualsIgnoreCase(it->second, "true")) {
51+
if (PropertyUtil::PropertyAsBoolean(properties, AuthProperties::kSigV4Enabled,
52+
/*default_value=*/false)) {
5253
return AuthProperties::kAuthTypeSigV4;
5354
}
5455

‎src/iceberg/catalog/rest/rest_catalog.cc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ Result<std::shared_ptr<MetricsReporter>> RestCatalog::MakeTableReporter(
523523
const TableIdentifier& identifier,
524524
const std::shared_ptr<auth::AuthSession>& table_session) const {
525525
auto metrics_enabled = config_.Get(RestCatalogProperties::kMetricsReportingEnabled);
526-
if (StringUtils::ToLower(metrics_enabled) == "true" &&
526+
if (StringUtils::ParseBoolean(metrics_enabled) &&
527527
supported_endpoints_.contains(Endpoint::ReportMetrics())) {
528528
ICEBERG_ASSIGN_OR_RAISE(auto path, paths_->Metrics(identifier));
529529
auto post = [client = client_](const std::string& endpoint, const std::string& body,

‎src/iceberg/test/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ add_iceberg_test(util_test
143143
roaring_position_bitmap_test.cc
144144
position_delete_index_test.cc
145145
position_delete_range_consumer_test.cc
146+
property_util_test.cc
146147
resolving_file_io_test.cc
147148
retry_util_test.cc
148149
string_util_test.cc

‎src/iceberg/test/arrow_s3_file_io_test.cc‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,13 @@ TEST_F(ArrowS3FileIOTest, RejectsIncompleteStaticCredentials) {
245245
"S3 client access key ID and secret access key must be set"));
246246
}
247247

248-
TEST_F(ArrowS3FileIOTest, RejectsInvalidBooleanProperties) {
249-
auto result =
250-
MakeS3FileIO({{std::string(S3Properties::kPathStyleAccess), "not-a-bool"}});
251-
EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument));
248+
TEST_F(ArrowS3FileIOTest, InvalidBooleanPropertyReadsAsFalse) {
249+
// As in Java, where these properties go through PropertyUtil.propertyAsBoolean, a
250+
// value that does not spell a boolean reads as false instead of being rejected.
251+
auto options =
252+
ConfigureS3Options({{std::string(S3Properties::kPathStyleAccess), "not-a-bool"}});
253+
ASSERT_THAT(options, IsOk());
254+
EXPECT_TRUE(options->force_virtual_addressing);
252255
}
253256

254257
TEST_F(ArrowS3FileIOTest, ReadWrite) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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/util/property_util.h"
21+
22+
#include <optional>
23+
#include <string>
24+
#include <string_view>
25+
#include <unordered_map>
26+
27+
#include <gtest/gtest.h>
28+
29+
namespace iceberg {
30+
31+
namespace {
32+
33+
constexpr std::string_view kKey = "test.enabled";
34+
35+
std::unordered_map<std::string, std::string> Properties(std::string_view value) {
36+
return {{std::string(kKey), std::string(value)}};
37+
}
38+
39+
} // namespace
40+
41+
TEST(PropertyUtilTest, BooleanDefaultsWhenPropertyIsAbsent) {
42+
const std::unordered_map<std::string, std::string> empty;
43+
EXPECT_TRUE(PropertyUtil::PropertyAsBoolean(empty, kKey, true));
44+
EXPECT_FALSE(PropertyUtil::PropertyAsBoolean(empty, kKey, false));
45+
EXPECT_EQ(PropertyUtil::PropertyAsOptionalBoolean(empty, kKey), std::nullopt);
46+
}
47+
48+
TEST(PropertyUtilTest, BooleanIgnoresCase) {
49+
for (const auto* value : {"true", "TRUE", "TrUe"}) {
50+
EXPECT_TRUE(PropertyUtil::PropertyAsBoolean(Properties(value), kKey, false)) << value;
51+
EXPECT_EQ(PropertyUtil::PropertyAsOptionalBoolean(Properties(value), kKey), true)
52+
<< value;
53+
}
54+
for (const auto* value : {"false", "FALSE", "FaLsE"}) {
55+
EXPECT_FALSE(PropertyUtil::PropertyAsBoolean(Properties(value), kKey, true)) << value;
56+
EXPECT_EQ(PropertyUtil::PropertyAsOptionalBoolean(Properties(value), kKey), false)
57+
<< value;
58+
}
59+
}
60+
61+
TEST(PropertyUtilTest, NonBooleanValueReadsAsFalse) {
62+
// Matches Java's PropertyUtil.propertyAsBoolean: a value that does not spell a
63+
// boolean is not rejected, and the default does not apply either, since the
64+
// property is set.
65+
for (const auto* value : {"", " true", "yes", "1", "ture"}) {
66+
EXPECT_FALSE(PropertyUtil::PropertyAsBoolean(Properties(value), kKey, true)) << value;
67+
EXPECT_EQ(PropertyUtil::PropertyAsOptionalBoolean(Properties(value), kKey), false)
68+
<< value;
69+
}
70+
}
71+
72+
} // namespace iceberg

‎src/iceberg/test/string_util_test.cc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ TEST(StringUtilsTest, ToLower) {
3232
ASSERT_EQ(StringUtils::ToLower("123"), "123");
3333
}
3434

35+
TEST(StringUtilsTest, ParseBoolean) {
36+
for (const auto* value : {"true", "TRUE", "TrUe"}) {
37+
EXPECT_TRUE(StringUtils::ParseBoolean(value)) << value;
38+
}
39+
// As in Java's Boolean.parseBoolean, a value that is not "true" reads as false
40+
// whether it spells a boolean or not.
41+
for (const auto* value :
42+
{"false", "FALSE", "FaLsE", "", " true", "true ", "yes", "1", "ture"}) {
43+
EXPECT_FALSE(StringUtils::ParseBoolean(value)) << value;
44+
}
45+
}
46+
3547
TEST(StringUtilsTest, ToUpper) {
3648
ASSERT_EQ(StringUtils::ToUpper("abc"), "ABC");
3749
ASSERT_EQ(StringUtils::ToUpper("A-bC"), "A-BC");

‎src/iceberg/util/config.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ U DefaultFromString(const std::string& val) {
5454
if constexpr (std::is_same_v<U, std::string>) {
5555
return val;
5656
} else if constexpr (std::is_same_v<U, bool>) {
57-
return StringUtils::EqualsIgnoreCase(val, "true");
57+
return StringUtils::ParseBoolean(val);
5858
} else if constexpr ((std::is_signed_v<U> && std::is_integral_v<U>) ||
5959
std::is_floating_point_v<U>) {
6060
ICEBERG_ASSIGN_OR_THROW(auto res, StringUtils::ParseNumber<U>(val));

‎src/iceberg/util/property_util.cc‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "iceberg/util/property_util.h"
2121

2222
#include <cstdint>
23+
#include <string>
2324

2425
#include "iceberg/table_properties.h"
2526
#include "iceberg/util/string_util.h"
@@ -46,4 +47,20 @@ Status PropertyUtil::ValidateCommitProperties(
4647
return {};
4748
}
4849

50+
bool PropertyUtil::PropertyAsBoolean(
51+
const std::unordered_map<std::string, std::string>& properties, std::string_view key,
52+
bool default_value) {
53+
return PropertyAsOptionalBoolean(properties, key).value_or(default_value);
54+
}
55+
56+
std::optional<bool> PropertyUtil::PropertyAsOptionalBoolean(
57+
const std::unordered_map<std::string, std::string>& properties,
58+
std::string_view key) {
59+
auto it = properties.find(std::string(key));
60+
if (it == properties.end()) {
61+
return std::nullopt;
62+
}
63+
return StringUtils::ParseBoolean(it->second);
64+
}
65+
4966
} // namespace iceberg

‎src/iceberg/util/property_util.h‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
/// \file iceberg/util/property_util.h
2323
/// \brief Provide property conversion helpers.
2424

25+
#include <optional>
2526
#include <string>
27+
#include <string_view>
2628
#include <unordered_map>
2729

2830
#include "iceberg/iceberg_export.h"
@@ -34,6 +36,33 @@ class ICEBERG_EXPORT PropertyUtil {
3436
public:
3537
static Status ValidateCommitProperties(
3638
const std::unordered_map<std::string, std::string>& properties);
39+
40+
/// \brief Read a boolean property from a property map.
41+
///
42+
/// Mirrors Java's PropertyUtil.propertyAsBoolean: the value is parsed with
43+
/// StringUtils::ParseBoolean, so anything that is not "true" ignoring case reads as
44+
/// false rather than being rejected.
45+
///
46+
/// \param properties The property map to read from.
47+
/// \param key The property key.
48+
/// \param default_value Returned when the property is absent.
49+
/// \return The parsed value, or default_value if the property is absent.
50+
static bool PropertyAsBoolean(
51+
const std::unordered_map<std::string, std::string>& properties,
52+
std::string_view key, bool default_value);
53+
54+
/// \brief Read a boolean property that may be unset.
55+
///
56+
/// Like PropertyAsBoolean, but returns std::nullopt when the property is absent so
57+
/// callers can distinguish an unset property from an explicit "false". Mirrors Java's
58+
/// PropertyUtil.propertyAsNullableBoolean.
59+
///
60+
/// \param properties The property map to read from.
61+
/// \param key The property key.
62+
/// \return The parsed value, or std::nullopt if the property is absent.
63+
static std::optional<bool> PropertyAsOptionalBoolean(
64+
const std::unordered_map<std::string, std::string>& properties,
65+
std::string_view key);
3766
};
3867

3968
} // namespace iceberg

0 commit comments

Comments
 (0)