laskoviymishka commented on code in PR #1575:
URL: https://github.com/apache/iceberg-go/pull/1575#discussion_r3681100146


##########
data_file_refs.go:
##########
@@ -0,0 +1,35 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package iceberg
+
+import "github.com/apache/iceberg-go/internal"
+
+// DataFileStatsRef returns statistics maps owned by the data file. The token
+// restricts this zero-copy accessor to trusted in-module callers; the public
+// DataFile getters continue returning defensive copies.
+func (d *dataFile) DataFileStatsRef(_ internal.DataFileRef) (
+       valueCounts map[int]int64,
+       nullCounts map[int]int64,
+       nanCounts map[int]int64,
+       lowerBounds map[int][]byte,
+       upperBounds map[int][]byte,
+) {
+       d.initColumnStatsData()
+
+       return d.valCntMap, d.nullCntMap, d.nanCntMap, d.lowerBoundMap, 
d.upperBoundMap

Review Comment:
   This is the one I'd want settled before merge.
   
   The PR is about protecting DataFile state from callers, but the `[]byte` 
values in the `lowerBounds`/`upperBounds` returned here are the same backing 
arrays as `d.lowerBoundMap`/`d.upperBoundMap` — a caller doing 
`lowerBounds[id][0] = x` writes straight through into cached state, and since 
`initColumnStatsData` is a `sync.Once` it never rebuilds.
   
   Both current callers are read-only so there's no live bug, but "zero-copy 
access to immutable DataFile state" claims more than the code delivers — the 
maps are shared and the byte values are mutable. At minimum, document on this 
method that callers must treat the returned slices as read-only, so the next 
caller wiring into this path doesn't corrupt bounds by accident.



##########
manifest.go:
##########
@@ -2201,7 +2202,45 @@ func (d *dataFile) FileFormat() FileFormat            { 
return d.Format }
 func (d *dataFile) Partition() map[int]any {
        d.initPartitionData()
 
-       return d.fieldIDToPartitionData
+       return clonePartitionMap(d.fieldIDToPartitionData)

Review Comment:
   `Partition()` is on the same per-file scan path as the stats getters 
(partition pruning calls it once per file, plus once per equality-delete/data 
pair) and now clones unconditionally, while stats get the `DataFileStatsRef` 
fast path — so the optimization is asymmetric.
   
   More to the point: the escape hatch adds a new internal package, a token 
type, an interface, and a runtime assertion to avoid five map copies per eval, 
and there's no benchmark showing that copy was ever hot. This PR needs a 
benchmark that demonstrates the saving (e.g. `BenchmarkInclusiveMetricsEval` 
with and without the ref path) before we take on this machinery — if it doesn't 
move the numbers, the escape hatch should come out and we just copy. Either way 
the result has to reconcile with `Partition()` copying unconditionally on the 
same path.



##########
manifest.go:
##########
@@ -2201,7 +2202,45 @@ func (d *dataFile) FileFormat() FileFormat            { 
return d.Format }
 func (d *dataFile) Partition() map[int]any {
        d.initPartitionData()
 
-       return d.fieldIDToPartitionData
+       return clonePartitionMap(d.fieldIDToPartitionData)
+}
+
+func clonePartitionMap(src map[int]any) map[int]any {
+       if src == nil {
+               return nil
+       }
+
+       out := maps.Clone(src)
+       for id, value := range out {
+               if bytes, ok := value.([]byte); ok {

Review Comment:
   `clonePartitionMap` only deep-copies `[]byte` — every other `any` is carried 
through by the shallow `maps.Clone`, so the isolation guarantee is really 
"`[]byte` only".
   
   For Avro-decoded files that's fine, since `convertAvroValueToIcebergType` 
only produces value types. But `NewDataFileBuilder` takes a caller-supplied 
`map[int]any`, and nothing stops a caller putting a `*big.Rat` or a nested map 
in there — those still alias after the clone, so the stated protection doesn't 
hold on the builder path. I'd document that isolation only holds for the 
decoder's concrete types, or handle the unsupported case explicitly.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to