Skip to content

Commit 4c906d1

Browse files
authored
feat: make ToLower Unicode-aware via utf8proc (#760)
Replaces the ASCII-only `StringUtils::ToLower` with a Unicode-aware implementation backed by [utf8proc](https://github.com/JuliaStrings/utf8proc), aligning case-insensitive name handling with Iceberg Java's `toLowerCase(Locale.ROOT)` for simple (1:1) case mappings. The few code points where simple and full case mapping differ (e.g. `İ` U+0130) are tracked in #808. - `ToLower` lower-cases UTF-8 using utf8proc simple (1:1) case mapping (e.g. `CAFÉ` → `café`, `GROẞE` → `große`). Pure-ASCII input takes a byte-wise fast path and never touches utf8proc. - Invalid UTF-8 is handled as a correctness guarantee (raised in review): `ToLower` is total — a byte that does not begin a valid UTF-8 sequence is passed through unchanged and decoding resumes at the next byte, so the valid code points around it are still lower-cased. The comparisons below therefore compare invalid bytes verbatim. - `EqualsIgnoreCase` and `StartsWithIgnoreCase` fold through `ToLower`, so they are case-insensitive for non-ASCII letters too. Both take an allocation-free ASCII fast path and fall back to `ToLower` only when a non-ASCII byte is present. - `StartsWithIgnoreCase` no longer assumes case mapping preserves byte length — the old byte-slice guard mishandled length-changing maps (e.g. `İ` U+0130 → `i`). Now `"İx"` starts with `"i"`, and `"i"` starts with `"İ"`. - `ToUpper` is intentionally left ASCII-only — it only normalizes ASCII enum/codec strings, and simple case mapping would be wrong for some letters (e.g. `ß` would stay unchanged instead of becoming `SS`). - utf8proc is pinned to 2.10.0 and wired into both the CMake (vendored via FetchContent / system package) and Meson (`subprojects/utf8proc.wrap`) builds, with matching source hashes and correct static-vs-shared linkage on Windows. ## Testing - `string_util_test.cc`: `ToLowerUnicode` (incl. invalid / truncated / stray-continuation bytes), `ToUpperAsciiOnly`, and Unicode + invalid-UTF-8 cases for `EqualsIgnoreCase` / `StartsWithIgnoreCase`. - `IgnoreCaseAgreesWithToLowerOracle` exhaustively checks both ASCII fast paths against the `ToLower` oracle over all short strings drawn from an alphabet with length-changing maps (`İ`→`i`, `K`→`k`) and invalid UTF-8. ## Remaining work (tracked in #808) - Full case-mapping parity with Java for the code points where simple ≠ full mapping (e.g. `İ` → `i̇`). - A streaming (allocation-free) non-ASCII path for `EqualsIgnoreCase` / `StartsWithIgnoreCase`. Part of #613 — does not fully fix it; #808 holds the design and remaining plan. Follow-up to #748.
1 parent ebc0e07 commit 4c906d1

10 files changed

Lines changed: 484 additions & 37 deletions

File tree

LICENSE

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,95 @@ Home page: https://arrow.apache.org/
228228
License: https://www.apache.org/licenses/LICENSE-2.0
229229

230230
--------------------------------------------------------------------------------
231+
232+
This product bundles utf8proc, which is available under the MIT License:
233+
234+
utf8proc is a software package originally developed by Jan Behrens and the rest
235+
of the Public Software Group, now maintained by the Julia-language developers.
236+
All new work on the utf8proc library is licensed under the MIT "expat" license:
237+
238+
Copyright (c) 2014-2021 by Steven G. Johnson, Jiahao Chen, Tony Kelman, Jonas
239+
Fonseca, and other contributors listed in the git history.
240+
241+
Permission is hereby granted, free of charge, to any person obtaining a
242+
copy of this software and associated documentation files (the "Software"),
243+
to deal in the Software without restriction, including without limitation
244+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
245+
and/or sell copies of the Software, and to permit persons to whom the
246+
Software is furnished to do so, subject to the following conditions:
247+
248+
The above copyright notice and this permission notice shall be included in
249+
all copies or substantial portions of the Software.
250+
251+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
252+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
253+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
254+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
255+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
256+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
257+
DEALINGS IN THE SOFTWARE.
258+
259+
The original utf8proc is licensed under the same MIT "expat" license:
260+
261+
Copyright (c) 2009, 2013 Public Software Group e. V., Berlin, Germany
262+
263+
Permission is hereby granted, free of charge, to any person obtaining a
264+
copy of this software and associated documentation files (the "Software"),
265+
to deal in the Software without restriction, including without limitation
266+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
267+
and/or sell copies of the Software, and to permit persons to whom the
268+
Software is furnished to do so, subject to the following conditions:
269+
270+
The above copyright notice and this permission notice shall be included in
271+
all copies or substantial portions of the Software.
272+
273+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
274+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
275+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
276+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
277+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
278+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
279+
DEALINGS IN THE SOFTWARE.
280+
281+
utf8proc also contains data derived from the Unicode data files. The following
282+
license applies to that data:
283+
284+
COPYRIGHT AND PERMISSION NOTICE
285+
286+
Copyright (c) 1991-2007 Unicode, Inc. All rights reserved. Distributed under
287+
the Terms of Use in http://www.unicode.org/copyright.html.
288+
289+
Permission is hereby granted, free of charge, to any person obtaining a
290+
copy of the Unicode data files and any associated documentation (the "Data
291+
Files") or Unicode software and any associated documentation (the
292+
"Software") to deal in the Data Files or Software without restriction,
293+
including without limitation the rights to use, copy, modify, merge,
294+
publish, distribute, and/or sell copies of the Data Files or Software, and
295+
to permit persons to whom the Data Files or Software are furnished to do
296+
so, provided that (a) the above copyright notice(s) and this permission
297+
notice appear with all copies of the Data Files or Software, (b) both the
298+
above copyright notice(s) and this permission notice appear in associated
299+
documentation, and (c) there is clear notice in each modified Data File or
300+
in the Software as well as in the documentation associated with the Data
301+
File(s) or Software that the data or software has been modified.
302+
303+
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
304+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
305+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
306+
THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
307+
INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR
308+
CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
309+
USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
310+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
311+
PERFORMANCE OF THE DATA FILES OR SOFTWARE.
312+
313+
Except as contained in this notice, the name of a copyright holder shall
314+
not be used in advertising or otherwise to promote the sale, use or other
315+
dealings in these Data Files or Software without prior written
316+
authorization of the copyright holder.
317+
318+
Unicode and the Unicode logo are trademarks of Unicode, Inc., and may be
319+
registered in some jurisdictions. All other trademarks and registered
320+
trademarks mentioned herein are the property of their respective owners.
321+
322+
--------------------------------------------------------------------------------

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ If you experience network issues when downloading dependencies, you can customiz
151151
- `ICEBERG_AVRO_GIT_URL`: Apache Avro git repository URL
152152
- `ICEBERG_NANOARROW_URL`: Nanoarrow tarball URL
153153
- `ICEBERG_CROARING_URL`: CRoaring tarball URL
154+
- `ICEBERG_UTF8PROC_URL`: utf8proc tarball URL
154155
- `ICEBERG_NLOHMANN_JSON_URL`: nlohmann-json tarball URL
155156
- `ICEBERG_SPDLOG_URL`: spdlog tarball URL
156157
- `ICEBERG_CPR_URL`: cpr tarball URL

cmake_modules/IcebergThirdpartyToolchain.cmake

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ endfunction()
8383
# ICEBERG_AVRO_GIT_URL - Apache Avro git repository URL
8484
# ICEBERG_NANOARROW_URL - Nanoarrow tarball URL
8585
# ICEBERG_CROARING_URL - CRoaring tarball URL
86+
# ICEBERG_UTF8PROC_URL - utf8proc tarball URL
8687
# ICEBERG_NLOHMANN_JSON_URL - nlohmann-json tarball URL
8788
# ICEBERG_SPDLOG_URL - spdlog tarball URL
8889
# ICEBERG_CPR_URL - cpr tarball URL
@@ -120,6 +121,20 @@ else()
120121
)
121122
endif()
122123

124+
set(ICEBERG_UTF8PROC_BUILD_VERSION "2.10.0")
125+
set(ICEBERG_UTF8PROC_BUILD_SHA256_CHECKSUM
126+
"276a37dc4d1dd24d7896826a579f4439d1e5fe33603add786bb083cab802e23e")
127+
128+
if(DEFINED ENV{ICEBERG_UTF8PROC_URL})
129+
set(UTF8PROC_SOURCE_URL "$ENV{ICEBERG_UTF8PROC_URL}")
130+
else()
131+
# Use the release asset (stable bytes, matching subprojects/utf8proc.wrap) rather
132+
# than the auto-generated tag archive, whose contents GitHub does not guarantee.
133+
set(UTF8PROC_SOURCE_URL
134+
"https://github.com/JuliaStrings/utf8proc/releases/download/v${ICEBERG_UTF8PROC_BUILD_VERSION}/utf8proc-${ICEBERG_UTF8PROC_BUILD_VERSION}.tar.gz"
135+
)
136+
endif()
137+
123138
# ----------------------------------------------------------------------
124139
# FetchContent
125140

@@ -436,6 +451,61 @@ function(resolve_croaring_dependency)
436451
PARENT_SCOPE)
437452
endfunction()
438453

454+
# ----------------------------------------------------------------------
455+
# utf8proc
456+
457+
function(resolve_utf8proc_dependency)
458+
prepare_fetchcontent()
459+
460+
# The vendored build needs no install rules; without this, CMake < 3.28 (where
461+
# FetchContent has no EXCLUDE_FROM_ALL) would install utf8proc's headers and
462+
# pkg-config file into the iceberg install prefix.
463+
set(UTF8PROC_INSTALL OFF)
464+
465+
fetchcontent_declare(utf8proc
466+
${FC_DECLARE_COMMON_OPTIONS}
467+
URL ${UTF8PROC_SOURCE_URL}
468+
URL_HASH "SHA256=${ICEBERG_UTF8PROC_BUILD_SHA256_CHECKSUM}"
469+
FIND_PACKAGE_ARGS
470+
NAMES
471+
utf8proc
472+
CONFIG)
473+
fetchcontent_makeavailable(utf8proc)
474+
475+
if(utf8proc_SOURCE_DIR)
476+
if(NOT TARGET utf8proc::utf8proc)
477+
add_library(utf8proc::utf8proc INTERFACE IMPORTED)
478+
target_link_libraries(utf8proc::utf8proc INTERFACE utf8proc)
479+
target_include_directories(utf8proc::utf8proc INTERFACE ${utf8proc_SOURCE_DIR})
480+
endif()
481+
482+
set(UTF8PROC_VENDORED TRUE)
483+
# utf8proc's CMake puts a raw build-tree path in INTERFACE_INCLUDE_DIRECTORIES, which
484+
# install(EXPORT) rejects. Wrap it in BUILD_INTERFACE so the export is valid; utf8proc
485+
# is a private dependency, so installed consumers never need its headers.
486+
set_target_properties(utf8proc
487+
PROPERTIES OUTPUT_NAME "iceberg_vendored_utf8proc"
488+
POSITION_INDEPENDENT_CODE ON
489+
INTERFACE_INCLUDE_DIRECTORIES
490+
"$<BUILD_INTERFACE:${utf8proc_SOURCE_DIR}>")
491+
install(TARGETS utf8proc
492+
EXPORT iceberg_targets
493+
RUNTIME DESTINATION "${ICEBERG_INSTALL_BINDIR}"
494+
ARCHIVE DESTINATION "${ICEBERG_INSTALL_LIBDIR}"
495+
LIBRARY DESTINATION "${ICEBERG_INSTALL_LIBDIR}")
496+
else()
497+
set(UTF8PROC_VENDORED FALSE)
498+
list(APPEND ICEBERG_SYSTEM_DEPENDENCIES utf8proc)
499+
endif()
500+
501+
set(ICEBERG_SYSTEM_DEPENDENCIES
502+
${ICEBERG_SYSTEM_DEPENDENCIES}
503+
PARENT_SCOPE)
504+
set(UTF8PROC_VENDORED
505+
${UTF8PROC_VENDORED}
506+
PARENT_SCOPE)
507+
endfunction()
508+
439509
# ----------------------------------------------------------------------
440510
# nlohmann-json
441511

@@ -734,6 +804,7 @@ endfunction()
734804
resolve_zlib_dependency()
735805
resolve_nanoarrow_dependency()
736806
resolve_croaring_dependency()
807+
resolve_utf8proc_dependency()
737808
resolve_nlohmann_json_dependency()
738809
resolve_spdlog_dependency()
739810

mkdocs/docs/getting-started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ If you experience network issues when downloading dependencies, you can override
143143
| `ICEBERG_AVRO_GIT_URL` | Apache Avro git repository |
144144
| `ICEBERG_NANOARROW_URL` | Nanoarrow tarball |
145145
| `ICEBERG_CROARING_URL` | CRoaring tarball |
146+
| `ICEBERG_UTF8PROC_URL` | utf8proc tarball |
146147
| `ICEBERG_NLOHMANN_JSON_URL` | nlohmann-json tarball |
147148
| `ICEBERG_CPR_URL` | cpr tarball |
148149

src/iceberg/CMakeLists.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,23 +154,27 @@ list(APPEND
154154
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,nanoarrow::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_static>,nanoarrow::nanoarrow_static,nanoarrow::nanoarrow_shared>>"
155155
nlohmann_json::nlohmann_json
156156
spdlog::spdlog
157+
utf8proc::utf8proc
157158
ZLIB::ZLIB)
158159
list(APPEND
159160
ICEBERG_SHARED_BUILD_INTERFACE_LIBS
160161
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,nanoarrow::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_shared>,nanoarrow::nanoarrow_shared,nanoarrow::nanoarrow_static>>"
161162
nlohmann_json::nlohmann_json
162163
spdlog::spdlog
164+
utf8proc::utf8proc
163165
ZLIB::ZLIB)
164166
list(APPEND
165167
ICEBERG_STATIC_INSTALL_INTERFACE_LIBS
166168
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,iceberg::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_static>,nanoarrow::nanoarrow_static,nanoarrow::nanoarrow_shared>>"
167169
"$<IF:$<BOOL:${NLOHMANN_JSON_VENDORED}>,iceberg::nlohmann_json,$<IF:$<TARGET_EXISTS:nlohmann_json::nlohmann_json>,nlohmann_json::nlohmann_json,nlohmann_json::nlohmann_json>>"
168-
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>")
170+
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>"
171+
"$<IF:$<BOOL:${UTF8PROC_VENDORED}>,iceberg::utf8proc,utf8proc::utf8proc>")
169172
list(APPEND
170173
ICEBERG_SHARED_INSTALL_INTERFACE_LIBS
171174
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,iceberg::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_shared>,nanoarrow::nanoarrow_shared,nanoarrow::nanoarrow_static>>"
172175
"$<IF:$<BOOL:${NLOHMANN_JSON_VENDORED}>,iceberg::nlohmann_json,$<IF:$<TARGET_EXISTS:nlohmann_json::nlohmann_json>,nlohmann_json::nlohmann_json,nlohmann_json::nlohmann_json>>"
173-
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>")
176+
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>"
177+
"$<IF:$<BOOL:${UTF8PROC_VENDORED}>,iceberg::utf8proc,utf8proc::utf8proc>")
174178

175179
add_iceberg_lib(iceberg
176180
SOURCES

src/iceberg/meson.build

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,27 @@ croaring_dep = dependency('croaring', static: croaring_needs_static)
198198
nanoarrow_dep = dependency('nanoarrow')
199199
nlohmann_json_dep = dependency('nlohmann_json')
200200
spdlog_dep = dependency('spdlog')
201+
# utf8proc's header declares its functions __declspec(dllimport) on Windows unless
202+
# UTF8PROC_STATIC is defined, and the wrap does not propagate that define to consumers.
203+
# Define it whenever utf8proc is linked statically, so the header's declarations match
204+
# how it is linked. Harmless on other platforms.
205+
utf8proc_needs_static = get_option('default_library') == 'static'
206+
utf8proc_dep = dependency('libutf8proc', static: utf8proc_needs_static)
207+
if utf8proc_needs_static
208+
utf8proc_dep = declare_dependency(
209+
compile_args: ['-DUTF8PROC_STATIC'],
210+
dependencies: utf8proc_dep,
211+
)
212+
endif
201213
zlib_dep = dependency('zlib')
202214

203-
iceberg_deps = [nanoarrow_dep, nlohmann_json_dep, spdlog_dep, zlib_dep]
215+
iceberg_deps = [
216+
nanoarrow_dep,
217+
nlohmann_json_dep,
218+
spdlog_dep,
219+
utf8proc_dep,
220+
zlib_dep,
221+
]
204222

205223
iceberg_lib = library(
206224
'iceberg',

0 commit comments

Comments
 (0)