You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ze...@apache.org on 2025/09/03 19:23:08 UTC

(iceberg-go) branch main updated: feat: implement object store location provider with hash-based path generation (#550)

This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 0a56519  feat: implement object store location provider with hash-based path generation (#550)
0a56519 is described below

commit 0a565193d51faaf4c0566331a4443bbfc11e647f
Author: Blue Li <10...@users.noreply.github.com>
AuthorDate: Thu Sep 4 03:23:02 2025 +0800

    feat: implement object store location provider with hash-based path generation (#550)
    
    Implements hash-based data file path generation for object stores,
    following
    [Java](https://github.com/apache/iceberg/blob/5d5e0a3559946a94be2979f3ff9a09f3b8e67f19/core/src/main/java/org/apache/iceberg/LocationProviders.java#L128)/[Python](https://github.com/apache/iceberg-python/blob/4f02298ee7506470e3485a47eec7ffdc72f71bfc/pyiceberg/table/locations.py#L116)
    implementations.
    
    Note: Partition keys are not considered as parameters since
    `PartitionSpec.PartitionToPath()` handles this, and partitioned table
    writes are not yet supported.
---
 table/locations.go      | 50 +++++++++++++++++++++++++++++++++++++++++++++++++
 table/locations_test.go | 26 +++++++++++++++++++++++++
 2 files changed, 76 insertions(+)

diff --git a/table/locations.go b/table/locations.go
index 46478cb..4e47fcb 100644
--- a/table/locations.go
+++ b/table/locations.go
@@ -20,9 +20,19 @@ package table
 import (
 	"fmt"
 	"net/url"
+	"path"
+	"strconv"
+	"strings"
 
 	"github.com/apache/iceberg-go"
 	"github.com/google/uuid"
+	"github.com/twmb/murmur3"
+)
+
+const (
+	hashBinaryStringBits = 20
+	entropyDirLength     = 4
+	entropyDirDepth      = 3
 )
 
 type LocationProvider interface {
@@ -96,6 +106,46 @@ type objectStoreLocationProvider struct {
 	includePartitionPaths bool
 }
 
+func computeHash(dataFileName string) string {
+	// Bitwise AND to combat sign-extension; bitwise OR to preserve leading zeroes
+	topMask := 1 << hashBinaryStringBits
+	hashCode := int(murmur3.Sum32([]byte(dataFileName)))&(topMask-1) | topMask
+
+	// Convert to binary string and take the last hashBinaryStringBits
+	binaryStr := strconv.FormatInt(int64(hashCode), 2)
+
+	return dirsFromHash(binaryStr[len(binaryStr)-hashBinaryStringBits:])
+}
+
+func dirsFromHash(fileHash string) string {
+	// Divides hash into directories for optimized orphan removal operation
+	totalEntropyLength := entropyDirDepth * entropyDirLength
+
+	hashWithDirs := make([]string, 0)
+	for i := 0; i < totalEntropyLength; i += entropyDirLength {
+		hashWithDirs = append(hashWithDirs, fileHash[i:i+entropyDirLength])
+	}
+
+	if len(fileHash) > totalEntropyLength {
+		hashWithDirs = append(hashWithDirs, fileHash[totalEntropyLength:])
+	}
+
+	return strings.Join(hashWithDirs, "/")
+}
+
+func (p *objectStoreLocationProvider) NewDataLocation(dataFileName string) string {
+	if path.Dir(dataFileName) != "." {
+		return p.simpleLocationProvider.NewDataLocation(dataFileName)
+	}
+
+	hashedPath := computeHash(dataFileName)
+	if p.includePartitionPaths {
+		return p.simpleLocationProvider.dataPath.JoinPath(hashedPath, dataFileName).String()
+	} else {
+		return p.simpleLocationProvider.dataPath.JoinPath(hashedPath + "-" + dataFileName).String()
+	}
+}
+
 func newObjectStoreLocationProvider(tableLoc *url.URL, tableProps iceberg.Properties) (*objectStoreLocationProvider, error) {
 	slp, err := newSimpleLocationProvider(tableLoc, tableProps)
 	if err != nil {
diff --git a/table/locations_test.go b/table/locations_test.go
index 2ef2e53..8fa08bc 100644
--- a/table/locations_test.go
+++ b/table/locations_test.go
@@ -74,3 +74,29 @@ func TestLocationProviderMetadataFileLocationCustomPath(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, "s3://table-location/custom/path/00001-30313233-3435-4637-b839-616263646566.metadata.json", loc)
 }
+
+func TestObjectStoreLocationProvider(t *testing.T) {
+	provider, err := table.LoadLocationProvider("table_location",
+		iceberg.Properties{table.ObjectStoreEnabledKey: "true"})
+	require.NoError(t, err)
+
+	assert.Equal(t, "table_location/data/0101/0110/1001/10110010/a", provider.NewDataLocation("a"))
+	assert.Equal(t, "table_location/data/1110/0111/1110/00000011/b", provider.NewDataLocation("b"))
+	assert.Equal(t, "table_location/data/0010/1101/0110/01011111/c", provider.NewDataLocation("c"))
+	assert.Equal(t, "table_location/data/1001/0001/0100/01110011/d", provider.NewDataLocation("d"))
+}
+
+// TestObjectStoreLocationProviderPartitionedPathsDisabled tests that when partitioned paths are disabled,
+// the final "/" is still replaced with "-" even for un-partitioned files. This matches the behavior of
+// the Java implementation.
+func TestObjectStoreLocationProviderPartitionedPathsDisabled(t *testing.T) {
+	provider, err := table.LoadLocationProvider("table_location",
+		iceberg.Properties{table.ObjectStoreEnabledKey: "true", table.WriteObjectStorePartitionedPathsKey: "false"})
+	require.NoError(t, err)
+
+	assert.Equal(t, "table_location/data/0101/0110/1001/10110010-a", provider.NewDataLocation("a"))
+	assert.Equal(t, "table_location/data/1110/0111/1110/00000011-b", provider.NewDataLocation("b"))
+	assert.Equal(t, "table_location/data/0010/1101/0110/01011111-c", provider.NewDataLocation("c"))
+	assert.Equal(t, "table_location/data/1001/0001/0100/01110011-d", provider.NewDataLocation("d"))
+	assert.Equal(t, "table_location/data/0110/1010/0011/11101000-test.parquet", provider.NewDataLocation("test.parquet"))
+}