Skip to content

Commit db69a2b

Browse files
committed
refactor(core): harden the Arrow 3 rewrite
Unify scan planning and column construction around bounded, single-pass seams. Restore ArrowTypes lowering and lifting, remove JSON3 integration, deepen conformance and deterministic fuzz coverage, update documentation, and prepare ArrowStrings 1.0. Preserve empty-scan schema metadata and charge every package-owned allocation.
1 parent f7f69c2 commit db69a2b

60 files changed

Lines changed: 24834 additions & 4525 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,31 @@ jobs:
194194
continue-on-error: false
195195
run: >
196196
julia --color=yes --project=monorepo -e 'using Pkg; Pkg.test("Arrow")'
197+
test_min_arrowtypes:
198+
name: Arrow.jl - ArrowTypes 2.0 compatibility
199+
runs-on: ubuntu-latest
200+
timeout-minutes: 15
201+
steps:
202+
- uses: actions/checkout@v7
203+
- uses: julia-actions/setup-julia@v3
204+
with:
205+
version: '1.10'
206+
project: '.'
207+
- uses: actions/cache@v6
208+
with:
209+
path: ~/.julia/artifacts
210+
key: min-arrowtypes-${{ runner.os }}-${{ hashFiles('Project.toml') }}
211+
restore-keys: min-arrowtypes-${{ runner.os }}-
212+
- name: Resolve the minimum supported ArrowTypes version
213+
shell: julia --project=. {0}
214+
run: |
215+
using Pkg
216+
Pkg.develop(PackageSpec(path="src/ArrowStrings"))
217+
Pkg.add(PackageSpec(url="https://github.com/JuliaData/Tables.jl", rev="64268c6a316e380cc3da26965f440a5433ebc1f7"))
218+
Pkg.add(PackageSpec(name="ArrowTypes", version=v"2.0.0"))
219+
Pkg.pin(PackageSpec(name="ArrowTypes"))
220+
- name: Test custom-type file and stream round trips
221+
run: julia --startup-file=no --history-file=no --project=. test/min_arrowtypes_compat.jl
197222
test_windows:
198223
name: ${{ matrix.pkg.name }} - Julia latest - Windows - 2 threads
199224
runs-on: windows-latest
@@ -235,7 +260,7 @@ jobs:
235260
with:
236261
project: ${{ matrix.pkg.dir }}
237262
trim:
238-
name: ArrowCore JuliaC trim-safe compile
263+
name: ArrowCore and ArrowStrings JuliaC trim-safe compile
239264
runs-on: ubuntu-latest
240265
timeout-minutes: 15
241266
steps:
@@ -248,7 +273,7 @@ jobs:
248273
path: ~/.julia/artifacts
249274
key: trim-${{ runner.os }}-${{ hashFiles('test/trim/Project.toml') }}
250275
restore-keys: trim-${{ runner.os }}-
251-
- name: Compile and run the trim-safe ArrowCore workload
276+
- name: Compile and run the trim-safe ArrowCore and ArrowStrings workload
252277
run: julia --startup-file=no --history-file=no test/trim_compile_tests.jl
253278
docs:
254279
name: Documentation

.github/workflows/fuzz.yml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
name: Deterministic fuzz
18+
19+
on:
20+
schedule:
21+
- cron: '15 3 * * 3'
22+
workflow_dispatch:
23+
inputs:
24+
seed:
25+
description: Master UInt64 seed (decimal or 0x-prefixed hex)
26+
required: false
27+
default: '0x9f5a37c241de880b'
28+
cases:
29+
description: Differential cases
30+
required: false
31+
default: '512'
32+
mutations:
33+
description: Byte mutations
34+
required: false
35+
default: '20000'
36+
37+
permissions:
38+
contents: read
39+
40+
concurrency:
41+
group: fuzz-${{ github.workflow }}-${{ github.ref }}
42+
cancel-in-progress: false
43+
44+
jobs:
45+
fuzz:
46+
name: Extended differential and mutation fuzz - stable Julia
47+
runs-on: ubuntu-latest
48+
timeout-minutes: 90
49+
env:
50+
FUZZ_SEED: ${{ github.event_name == 'schedule' && github.run_id || inputs.seed || '0x9f5a37c241de880b' }}
51+
FUZZ_CASES: ${{ inputs.cases || '512' }}
52+
FUZZ_MUTATIONS: ${{ inputs.mutations || '20000' }}
53+
FUZZ_REPRO_DIR: fuzz-reproductions
54+
steps:
55+
- uses: actions/checkout@v7
56+
- uses: julia-actions/setup-julia@v3
57+
with:
58+
version: '1'
59+
- uses: actions/cache@v6
60+
with:
61+
path: ~/.julia/artifacts
62+
key: fuzz-${{ runner.os }}-${{ hashFiles('**/Project.toml') }}
63+
restore-keys: fuzz-${{ runner.os }}-
64+
- name: Prepare development dependencies
65+
shell: julia --project=. {0}
66+
run: |
67+
using Pkg
68+
Pkg.develop([PackageSpec(path="src/ArrowStrings"), PackageSpec(path="src/ArrowTypes")])
69+
Pkg.add(PackageSpec(url="https://github.com/JuliaData/Tables.jl", rev="64268c6a316e380cc3da26965f440a5433ebc1f7"))
70+
- name: Report replay coordinates
71+
run: |
72+
echo "event=$GITHUB_EVENT_NAME"
73+
echo "master_seed=$FUZZ_SEED"
74+
if [ "$GITHUB_EVENT_NAME" = "schedule" ] && [ "$FUZZ_SEED" != "$GITHUB_RUN_ID" ]; then
75+
echo "scheduled fuzz seed must equal the workflow run ID" >&2
76+
exit 1
77+
fi
78+
- name: Run deterministic differential and mutation fuzzing
79+
run: >-
80+
timeout --signal=TERM --kill-after=30s 80m
81+
julia --color=yes --startup-file=no --history-file=no --project=.
82+
test/fuzz.jl
83+
--seed "$FUZZ_SEED"
84+
--cases "$FUZZ_CASES"
85+
--mutations "$FUZZ_MUTATIONS"
86+
--determinism-every 256
87+
--repro-dir "$FUZZ_REPRO_DIR"
88+
- name: Upload failure reproductions
89+
if: failure() && hashFiles('fuzz-reproductions/**') != ''
90+
uses: actions/upload-artifact@v6
91+
with:
92+
name: deterministic-fuzz-reproductions
93+
path: fuzz-reproductions
94+
if-no-files-found: ignore
95+
retention-days: 14

CHANGELOG.md

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ writer, validation, scan, and C interface engines.
8181
- Structural, semantic, and optional full-content validation tiers.
8282
- Resource limits for untrusted IPC metadata and buffers.
8383
- Apache Arrow gold-corpus tests, external IPC oracle tests, C interface oracle
84-
tests, and a JuliaC `--trim=safe` compile gate.
84+
tests, deterministic differential and bounded mutation fuzzing, and a JuliaC
85+
`--trim=safe` compile gate.
8586

8687
### Changed
8788

@@ -91,16 +92,73 @@ writer, validation, scan, and C interface engines.
9192
- A table read from Arrow retains compatible schema details when it is written
9293
again, including temporal units, dictionary encoding and category order,
9394
list widths, composite descriptors, nullability, and ordered duplicate
94-
metadata. Fresh heterogeneous Julia Union columns can be synthesized, but a
95-
retained Union still fails clearly after facade materialization discards its
96-
original routing. Nested Dictionaries fail clearly after their pool data is
97-
lost.
95+
metadata. Fresh heterogeneous Julia Union columns can be synthesized.
96+
Registered ArrowTypes.jl public-domain values can also reconstruct a retained Union
97+
from their writer-side type evidence, including dense or sparse mode and type
98+
IDs. An unregistered retained Union still fails clearly after facade
99+
materialization discards its original routing.
100+
Nested Dictionaries fail clearly after their pool data is lost.
98101
- Writing is validated before bytes are published to the output sink.
102+
- Fresh `Union{Missing, NamedTuple}` columns use Struct parent validity while
103+
preserving each child's declared nullability and type.
99104
- Custom values are lowered recursively through `ArrowTypes.ArrowType` and
100105
`ArrowTypes.toarrow`. Extension names and metadata are written, and reads use
101106
`ArrowTypes.JuliaType`, `ArrowTypes.fromarrow`, and
102107
`ArrowTypes.fromarrowstruct` to restore registered logical types. An unknown
103108
extension name warns and returns its storage value.
109+
- A fresh abstract column with no mapping of its own uses concrete subtype
110+
evidence across the complete column. Concrete subtype extensions are kept,
111+
and heterogeneous subtype evidence forms an explicit bounded Union instead
112+
of silently erasing subtype metadata.
113+
- Registered logical types whose storage is a Union preserve external child
114+
order, child labels, type IDs, dense or sparse mode, nested descriptor details,
115+
and outer-null routing on rewrite. Sparse children use canonical hidden
116+
placeholders outside their active rows. An abstract registered target accepts
117+
an extensionless concrete subtype or one with the retained parent identity; a
118+
different explicit identity fails closed. A logical storage Union that already
119+
uses `Missing` cannot also add an outer missing state because the two states
120+
have no distinguishable Arrow representation.
121+
- Hidden retained composite slots are built directly from their Field and
122+
logical length. Null-only fixed-size-list descendants do not allocate one
123+
Julia placeholder per hidden element, including partly missing fresh or
124+
retained columns and inactive sparse-Union rows. Sparse Union children also
125+
distinguish inactive physical slots from selected values, so inactive null
126+
storage does not weaken selected-value validation.
127+
- Recursive ArrowTypes.jl storage schemas, recursive value containers, and
128+
custom mapping nesting beyond 64 levels fail with `ArgumentError`. The
129+
removed `maxdepth` keyword does not make writer recursion unbounded.
130+
- Declared writer Unions support up to 32 branches. Runtime writer or storage
131+
inference accepts at most 8 distinct types across the complete column. This
132+
covers abstract ArrowTypes storage, abstract or `Any` dictionary values, and
133+
abstract retained ArrowTypes targets, and bounds per-type schema planning and
134+
compiler work.
135+
- ArrowTypes logical resolution uses exact fixed-size-list tuple signatures
136+
through arity 1024 and a compact tuple-family signature above that limit,
137+
preventing fixed-size-list sizes from causing proportional type allocation.
138+
Extension Struct signatures are exact through 1024 children only when child
139+
names are unique, contain no embedded NUL, already exist as Julia `Symbol`s,
140+
and pass the 4096-byte per-name and 64-KiB total limits. Otherwise labelled
141+
Structs remain unknown extensions and return ordered `Pair` storage. One
142+
bounded ArrowTypes.jl Tuple compatibility exception may intern only a
143+
complete canonical positional sequence `"1"`, `"2"`, …, `string(N)` through
144+
`N = 1024`. Unknown extension labels return before that check; arbitrary or
145+
partly positional Struct names are not interned.
146+
Any writer-side `ArrowType` result that is a concrete tuple above that limit
147+
is rejected before the writer specializes on the oversized storage shape.
148+
This includes ArrowTypes.jl's default mapping for a tuple value.
149+
- Core keeps schema names as strings. The Tables.jl facade preflights per-name,
150+
novel-name-count, and novel-name-byte limits before it interns top-level
151+
column names. Unknown ArrowTypes extension labels do not create Julia
152+
symbols. Unsupported-label warnings are deduplicated by label and capped at
153+
16 distinct labels plus one suppression notice per table materialization. A
154+
novel `JuliaLang.Symbol` IPC payload now raises `ValidationError` instead of
155+
being interned, so an input that an earlier Arrow.jl release read successfully
156+
can now fail.
157+
- Registered public-domain values can rebuild compatible retained binary, list,
158+
date-like, duration, wide-decimal, and interval descriptors. Retained widths,
159+
sizes, units, child fields, and sorted Map claims are checked before output.
160+
- Empty and typed all-missing registered columns use their concrete declared
161+
element types as schema evidence instead of bypassing retained-field checks.
104162
- Scan filters over fields that contain registered ArrowTypes.jl logical types
105163
at any depth evaluate over the public materialized values. The mapping
106164
interface does not require storage lowering to preserve Julia comparison

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
3232
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
3333
TranscodingStreams = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
3434

35-
# ArrowStrings and ArrowTypes live in this repository and are registered
36-
# separately; the source entries resolve them from their subdirectories.
35+
# ArrowStrings and ArrowTypes live in this repository and are released and
36+
# registered independently; the source entries resolve their subdirectories.
3737
# `Tables.Scan` is not in a Tables.jl release yet, so Tables
3838
# resolves from the reviewed prerequisite commit. Pkg reads `[sources]` from Julia 1.11;
3939
# on 1.10 the CI workflow adds both explicitly.

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,18 @@ Pkg.add("Arrow")
4848
## Quick start
4949

5050
```julia
51-
using Arrow, Tables
51+
using Arrow
5252

5353
data = (id = [1, 2, 3], name = ["Ada", "Babbage", missing])
5454
Arrow.write("data.arrow", data)
5555

5656
table = Arrow.Table("data.arrow")
57-
Tables.columnnames(table) # [:id, :name]
58-
collect(table.name) == ["Ada", "Babbage", missing] # true
57+
propertynames(table) # [:id, :name]
58+
isequal(collect(table.name), ["Ada", "Babbage", missing]) # true
5959
```
6060

6161
`Arrow.Table` accepts a path, an `IO`, IPC bytes, or an
62-
`Arrow.AbstractArrowSource`. `Arrow.Stream` reads one record batch at a time.
62+
`Arrow.AbstractArrowSource`. `Arrow.Stream` iterates one record batch at a time.
6363
`Arrow.write` accepts any Tables.jl source.
6464

6565
Arrow 3.0 includes:
@@ -76,7 +76,7 @@ Arrow 3.0 includes:
7676
Arrow 3.0 is a breaking rewrite. Read the
7777
[migration guide](docs/src/migration.md) before you update from Arrow 2.x.
7878
See the [changelog](CHANGELOG.md) for the full release summary. The
79-
[user manual](https://arrow.apache.org/julia/) and
79+
[user manual](docs/src/manual.md) and
8080
[API reference](docs/src/reference.md) describe the supported public API.
8181

8282
## Development
@@ -99,6 +99,14 @@ Nanoarrow IPC oracle checks, and PyArrow C interface checks. Run all of them
9999
with `julia conformance/run.jl`. Docker and network access for the first image
100100
build are required.
101101

102+
Run `julia --project=. test/fuzz.jl --cases 16 --mutations 64` for the
103+
deterministic PR-sized fuzz suite. The scheduled workflow runs the extended
104+
512-case and 20,000-mutation suite with a new reproducible master seed for each
105+
scheduled run. It repeats the first full route sweep and every 256th mutation
106+
to detect unstable outcomes. If the runner records or times out on a case, the
107+
workflow uploads its replay coordinates, mutated bytes when available, the
108+
resolved package environment, and a location-independent `replay.sh` wrapper.
109+
102110
The Arrow 3.0 rewrite used Anthropic Claude Code and OpenAI Codex for code
103111
generation, test generation, and review. Apache Arrow maintainers remain
104112
responsible for understanding, reviewing, testing, and approving the code and

codecov.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818
---
1919
codecov:
2020
notify:
21-
# Wait for all "test" matrix jobs
22-
after_n_builds: 48
21+
# 48 base matrix jobs minus the two ArrowTypes/min/macOS thread variants.
22+
after_n_builds: 46

0 commit comments

Comments
 (0)