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 db1c6829 refactor(io): use the registry pattern for IO schemes (#709)
db1c6829 is described below

commit db1c68299187f2cdbfaf72333882a0c2668a97cb
Author: Alessandro Nori <[email protected]>
AuthorDate: Mon Feb 23 22:38:07 2026 +0100

    refactor(io): use the registry pattern for IO schemes (#709)
    
    Related to https://github.com/apache/iceberg-go/issues/696
    
    This PR introduces a registry pattern for IO implementations, similar to
    the existing catalog package pattern.
    
     Moved all cloud storage implementations to `io/gocloud`.
    
    ### Extra notes
    I decided to use a single subpackage because all the existing
    implementations use the same dependency and it's easier to import just
    one package to register all of them. However I think in most of the
    integration tests only use `s3` so multiple subpackages would also work
    fine.
---
 .github/workflows/go-integration.yml       |   2 +-
 Makefile                                   |   2 +-
 catalog/glue/glue_test.go                  |   1 +
 catalog/rest/rest_integration_test.go      |   1 +
 catalog/sql/sql_integration_test.go        |   1 +
 catalog/sql/sql_test.go                    |   1 +
 cmd/iceberg/main.go                        |   1 +
 io/config.go                               |  55 +++++++++
 io/{ => gocloud}/azure.go                  |  31 ++---
 io/{ => gocloud}/azure_integration_test.go |  13 ++-
 io/{ => gocloud}/azure_test.go             |   2 +-
 io/{ => gocloud}/blob.go                   |   9 +-
 io/{ => gocloud}/blob_test.go              |   2 +-
 io/{ => gocloud}/gcs.go                    |  22 ++--
 io/{ => gocloud}/gcs_integration_test.go   |   5 +-
 io/gocloud/mem_test.go                     | 133 ++++++++++++++++++++++
 io/gocloud/register.go                     |  85 ++++++++++++++
 io/{ => gocloud}/s3.go                     |  35 ++----
 io/{ => gocloud}/s3_integration_test.go    |   3 +-
 io/{ => gocloud}/s3_test.go                |  23 ++--
 io/{ => gocloud}/utils.go                  |   2 +-
 io/io.go                                   |  74 ++++--------
 io/registry.go                             |  93 +++++++++++++++
 io/registry_test.go                        | 175 +++++++++++++++++++++++++++++
 table/orphan_cleanup_integration_test.go   |   1 +
 table/scanner_test.go                      |   1 +
 table/snapshot_producers_test.go           |   1 +
 table/transaction_test.go                  |   1 +
 28 files changed, 635 insertions(+), 140 deletions(-)

diff --git a/.github/workflows/go-integration.yml 
b/.github/workflows/go-integration.yml
index 6a6696cf..59bd0e9c 100644
--- a/.github/workflows/go-integration.yml
+++ b/.github/workflows/go-integration.yml
@@ -61,7 +61,7 @@ jobs:
           AWS_REGION: "us-east-1"
           SPARK_CONTAINER_ID: "${{ env.SPARK_CONTAINER_ID }}"
           DOCKER_API_VER: "${{ env.DOCKER_API_VER }}"
-        run: |          
+        run: |
           make integration-test
 
       - name: Show debug logs
diff --git a/Makefile b/Makefile
index 6458b041..580d89d3 100644
--- a/Makefile
+++ b/Makefile
@@ -40,7 +40,7 @@ integration-scanner:
        go test -tags=integration -v -run="^TestScanner" ./table
 
 integration-io:
-       go test -tags=integration -v ./io
+       go test -tags=integration -v ./io/...
 
 integration-rest:
        go test -tags=integration -v -run="^TestRestIntegration$$" 
./catalog/rest
diff --git a/catalog/glue/glue_test.go b/catalog/glue/glue_test.go
index c05757f5..ddadcdf5 100644
--- a/catalog/glue/glue_test.go
+++ b/catalog/glue/glue_test.go
@@ -31,6 +31,7 @@ import (
 
        "github.com/apache/iceberg-go"
        "github.com/apache/iceberg-go/catalog"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
        "github.com/aws/aws-sdk-go-v2/aws"
        "github.com/aws/aws-sdk-go-v2/config"
diff --git a/catalog/rest/rest_integration_test.go 
b/catalog/rest/rest_integration_test.go
index 17fe2026..ca156f62 100644
--- a/catalog/rest/rest_integration_test.go
+++ b/catalog/rest/rest_integration_test.go
@@ -32,6 +32,7 @@ import (
        "github.com/apache/iceberg-go/catalog"
        "github.com/apache/iceberg-go/catalog/rest"
        "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
        "github.com/apache/iceberg-go/view"
        "github.com/stretchr/testify/require"
diff --git a/catalog/sql/sql_integration_test.go 
b/catalog/sql/sql_integration_test.go
index 5e879e9d..82f83bba 100644
--- a/catalog/sql/sql_integration_test.go
+++ b/catalog/sql/sql_integration_test.go
@@ -34,6 +34,7 @@ import (
        "github.com/apache/iceberg-go/catalog"
        "github.com/apache/iceberg-go/catalog/sql"
        "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
        "github.com/stretchr/testify/suite"
 )
diff --git a/catalog/sql/sql_test.go b/catalog/sql/sql_test.go
index 6ac0fdb8..039189bc 100644
--- a/catalog/sql/sql_test.go
+++ b/catalog/sql/sql_test.go
@@ -36,6 +36,7 @@ import (
        "github.com/apache/iceberg-go/catalog"
        "github.com/apache/iceberg-go/catalog/internal"
        sqlcat "github.com/apache/iceberg-go/catalog/sql"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/suite"
diff --git a/cmd/iceberg/main.go b/cmd/iceberg/main.go
index 12838591..a42e92bb 100644
--- a/cmd/iceberg/main.go
+++ b/cmd/iceberg/main.go
@@ -31,6 +31,7 @@ import (
        "github.com/apache/iceberg-go/catalog/hive"
        "github.com/apache/iceberg-go/catalog/rest"
        "github.com/apache/iceberg-go/config"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
 
        awsconfig "github.com/aws/aws-sdk-go-v2/config"
diff --git a/io/config.go b/io/config.go
new file mode 100644
index 00000000..b94e1ddc
--- /dev/null
+++ b/io/config.go
@@ -0,0 +1,55 @@
+// 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 io
+
+// Constants for S3 configuration options
+const (
+       S3Region                 = "s3.region"
+       S3SessionToken           = "s3.session-token"
+       S3SecretAccessKey        = "s3.secret-access-key"
+       S3AccessKeyID            = "s3.access-key-id"
+       S3EndpointURL            = "s3.endpoint"
+       S3ProxyURI               = "s3.proxy-uri"
+       S3ConnectTimeout         = "s3.connect-timeout"
+       S3SignerURI              = "s3.signer.uri"
+       S3RemoteSigningEnabled   = "s3.remote-signing-enabled"
+       S3ForceVirtualAddressing = "s3.force-virtual-addressing"
+)
+
+// Constants for GCS configuration options
+const (
+       GCSEndpoint   = "gcs.endpoint"
+       GCSKeyPath    = "gcs.keypath"
+       GCSJSONKey    = "gcs.jsonkey"
+       GCSCredType   = "gcs.credtype"
+       GCSUseJSONAPI = "gcs.usejsonapi" // set to anything to enable
+)
+
+// Constants for Azure configuration options
+const (
+       ADLSSasTokenPrefix         = "adls.sas-token."
+       ADLSConnectionStringPrefix = "adls.connection-string."
+       ADLSSharedKeyAccountName   = "adls.auth.shared-key.account.name"
+       ADLSSharedKeyAccountKey    = "adls.auth.shared-key.account.key"
+       ADLSEndpoint               = "adls.endpoint"
+       ADLSProtocol               = "adls.protocol"
+
+       // Not in use yet
+       // ADLSReadBlockSize          = "adls.read.block-size-bytes"
+       // ADLSWriteBlockSize         = "adls.write.block-size-bytes"
+)
diff --git a/io/azure.go b/io/gocloud/azure.go
similarity index 87%
rename from io/azure.go
rename to io/gocloud/azure.go
index 224c1dc7..06da2e2c 100644
--- a/io/azure.go
+++ b/io/gocloud/azure.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import (
        "context"
@@ -28,6 +28,7 @@ import (
        "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+       "github.com/apache/iceberg-go/io"
        "gocloud.dev/blob"
        "gocloud.dev/blob/azureblob"
 )
@@ -36,20 +37,6 @@ import (
 // 
https://github.com/apache/iceberg/blob/2114bf631e49af532d66e2ce148ee49dd1dd1f1f/azure/src/main/java/org/apache/iceberg/azure/adlsv2/ADLSLocation.java#L47
 var adlsURIPattern = regexp.MustCompile(`^(abfss?|wasbs?)://([^/?#]+)(.*)?$`)
 
-// Constants for Azure configuration options
-const (
-       AdlsSasTokenPrefix         = "adls.sas-token."
-       AdlsConnectionStringPrefix = "adls.connection-string."
-       AdlsSharedKeyAccountName   = "adls.auth.shared-key.account.name"
-       AdlsSharedKeyAccountKey    = "adls.auth.shared-key.account.key"
-       AdlsEndpoint               = "adls.endpoint"
-       AdlsProtocol               = "adls.protocol"
-
-       // Not in use yet
-       // AdlsReadBlockSize          = "adls.read.block-size-bytes"
-       // AdlsWriteBlockSize         = "adls.write.block-size-bytes"
-)
-
 // adlsLocation represents the parsed components of an Azure Data Lake Storage 
URI
 type adlsLocation struct {
        accountName   string // Azure storage account name
@@ -121,8 +108,8 @@ func newAdlsLocation(adlsURI *url.URL) (*adlsLocation, 
error) {
 
 // Construct a Azure bucket from a URL
 func createAzureBucket(ctx context.Context, parsed *url.URL, props 
map[string]string) (*blob.Bucket, error) {
-       adlsSasTokens := propertiesWithPrefix(props, AdlsSasTokenPrefix)
-       adlsConnectionStrings := propertiesWithPrefix(props, 
AdlsConnectionStringPrefix)
+       adlsSasTokens := propertiesWithPrefix(props, io.ADLSSasTokenPrefix)
+       adlsConnectionStrings := propertiesWithPrefix(props, 
io.ADLSConnectionStringPrefix)
 
        // Construct the client
        location, err := newAdlsLocation(parsed)
@@ -130,16 +117,16 @@ func createAzureBucket(ctx context.Context, parsed 
*url.URL, props map[string]st
                return nil, err
        }
 
-       sharedKeyAccountName := props[AdlsSharedKeyAccountName]
-       endpoint := props[AdlsEndpoint]
-       protocol := props[AdlsProtocol]
+       sharedKeyAccountName := props[io.ADLSSharedKeyAccountName]
+       endpoint := props[io.ADLSEndpoint]
+       protocol := props[io.ADLSProtocol]
 
        var client *container.Client
 
        if sharedKeyAccountName != "" {
-               sharedKeyAccountKey, ok := props[AdlsSharedKeyAccountKey]
+               sharedKeyAccountKey, ok := props[io.ADLSSharedKeyAccountKey]
                if !ok || sharedKeyAccountKey == "" {
-                       return nil, fmt.Errorf("azure authentication: 
shared-key requires both %s and %s", AdlsSharedKeyAccountName, 
AdlsSharedKeyAccountKey)
+                       return nil, fmt.Errorf("azure authentication: 
shared-key requires both %s and %s", io.ADLSSharedKeyAccountName, 
io.ADLSSharedKeyAccountKey)
                }
 
                containerURL, err := createContainerURL(location.accountName, 
protocol, endpoint, "", location.containerName)
diff --git a/io/azure_integration_test.go b/io/gocloud/azure_integration_test.go
similarity index 94%
rename from io/azure_integration_test.go
rename to io/gocloud/azure_integration_test.go
index cfe5ecb6..cd699840 100644
--- a/io/azure_integration_test.go
+++ b/io/gocloud/azure_integration_test.go
@@ -17,7 +17,7 @@
 
 //go:build integration
 
-package io_test
+package gocloud_test
 
 import (
        "context"
@@ -30,6 +30,7 @@ import (
        "github.com/apache/iceberg-go/catalog"
        sqlcat "github.com/apache/iceberg-go/catalog/sql"
        "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/stretchr/testify/suite"
        "github.com/uptrace/bun/driver/sqliteshim"
        "gocloud.dev/blob/azureblob"
@@ -64,10 +65,10 @@ func (s *AzureBlobIOTestSuite) TestAzureBlobWarehouseKey() {
                sqlcat.DriverKey:            sqliteshim.ShimName,
                sqlcat.DialectKey:           string(sqlcat.SQLite),
                "type":                      "sql",
-               io.AdlsSharedKeyAccountName: accountName,
-               io.AdlsSharedKeyAccountKey:  accountKey,
-               io.AdlsEndpoint:             endpoint,
-               io.AdlsProtocol:             protocol,
+               io.ADLSSharedKeyAccountName: accountName,
+               io.ADLSSharedKeyAccountKey:  accountKey,
+               io.ADLSEndpoint:             endpoint,
+               io.ADLSProtocol:             protocol,
        }
 
        cat, err := catalog.Load(context.Background(), "default", properties)
@@ -99,7 +100,7 @@ func (s *AzureBlobIOTestSuite) 
TestAzuriteWarehouseConnectionString() {
                sqlcat.DriverKey:  sqliteshim.ShimName,
                sqlcat.DialectKey: string(sqlcat.SQLite),
                "type":            "sql",
-               io.AdlsConnectionStringPrefix + accountName: connectionString,
+               io.ADLSConnectionStringPrefix + accountName: connectionString,
        }
 
        cat, err := catalog.Load(context.Background(), "default", properties)
diff --git a/io/azure_test.go b/io/gocloud/azure_test.go
similarity index 99%
rename from io/azure_test.go
rename to io/gocloud/azure_test.go
index fbb39dd0..faa5cf58 100644
--- a/io/azure_test.go
+++ b/io/gocloud/azure_test.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import (
        "context"
diff --git a/io/blob.go b/io/gocloud/blob.go
similarity index 96%
rename from io/blob.go
rename to io/gocloud/blob.go
index 14faf33e..4c417f5a 100644
--- a/io/blob.go
+++ b/io/gocloud/blob.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import (
        "context"
@@ -26,6 +26,7 @@ import (
        "path/filepath"
        "strings"
 
+       icebergio "github.com/apache/iceberg-go/io"
        "gocloud.dev/blob"
 )
 
@@ -103,7 +104,7 @@ func (bfs *blobFileIO) preprocess(path string) (string, 
error) {
        return bfs.keyExtractor(path)
 }
 
-func (bfs *blobFileIO) Open(path string) (File, error) {
+func (bfs *blobFileIO) Open(path string) (icebergio.File, error) {
        var err error
        path, err = bfs.preprocess(path)
        if err != nil {
@@ -133,7 +134,7 @@ func (bfs *blobFileIO) Remove(name string) error {
        return bfs.Delete(bfs.ctx, name)
 }
 
-func (bfs *blobFileIO) Create(name string) (FileWriter, error) {
+func (bfs *blobFileIO) Create(name string) (icebergio.FileWriter, error) {
        return bfs.NewWriter(bfs.ctx, name, true, nil)
 }
 
@@ -185,7 +186,7 @@ func (bfs *blobFileIO) NewWriter(ctx context.Context, path 
string, overwrite boo
                nil
 }
 
-func createBlobFS(ctx context.Context, bucket *blob.Bucket, keyExtractor 
KeyExtractor) IO {
+func createBlobFS(ctx context.Context, bucket *blob.Bucket, keyExtractor 
KeyExtractor) icebergio.IO {
        return &blobFileIO{Bucket: bucket, keyExtractor: keyExtractor, ctx: ctx}
 }
 
diff --git a/io/blob_test.go b/io/gocloud/blob_test.go
similarity index 99%
rename from io/blob_test.go
rename to io/gocloud/blob_test.go
index c6029809..6854be7e 100644
--- a/io/blob_test.go
+++ b/io/gocloud/blob_test.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import (
        "context"
diff --git a/io/gcs.go b/io/gocloud/gcs.go
similarity index 84%
rename from io/gcs.go
rename to io/gocloud/gcs.go
index fa0de9ef..96e5e1dc 100644
--- a/io/gcs.go
+++ b/io/gocloud/gcs.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import (
        "context"
@@ -23,21 +23,13 @@ import (
 
        "cloud.google.com/go/storage"
 
+       "github.com/apache/iceberg-go/io"
        "gocloud.dev/blob"
        "gocloud.dev/blob/gcsblob"
        "gocloud.dev/gcp"
        "google.golang.org/api/option"
 )
 
-// Constants for GCS configuration options
-const (
-       GCSEndpoint   = "gcs.endpoint"
-       GCSKeyPath    = "gcs.keypath"
-       GCSJSONKey    = "gcs.jsonkey"
-       GCSCredType   = "gcs.credtype"
-       GCSUseJsonAPI = "gcs.usejsonapi" // set to anything to enable
-)
-
 var allowedGCSCredTypes = map[string]option.CredentialsType{
        "service_account":              option.ServiceAccount,
        "authorized_user":              option.AuthorizedUser,
@@ -48,22 +40,22 @@ var allowedGCSCredTypes = map[string]option.CredentialsType{
 // ParseGCSConfig parses GCS properties and returns a configuration.
 func ParseGCSConfig(props map[string]string) *gcsblob.Options {
        var o []option.ClientOption
-       if url := props[GCSEndpoint]; url != "" {
+       if url := props[io.GCSEndpoint]; url != "" {
                o = append(o, option.WithEndpoint(url))
        }
        var credType option.CredentialsType
-       if key := props[GCSCredType]; key != "" {
+       if key := props[io.GCSCredType]; key != "" {
                if ct, ok := allowedGCSCredTypes[key]; ok {
                        credType = ct
                }
        }
-       if key := props[GCSJSONKey]; key != "" {
+       if key := props[io.GCSJSONKey]; key != "" {
                o = append(o, option.WithAuthCredentialsJSON(credType, 
[]byte(key)))
        }
-       if path := props[GCSKeyPath]; path != "" {
+       if path := props[io.GCSKeyPath]; path != "" {
                o = append(o, option.WithAuthCredentialsFile(credType, path))
        }
-       if _, ok := props[GCSUseJsonAPI]; ok {
+       if _, ok := props[io.GCSUseJSONAPI]; ok {
                o = append(o, storage.WithJSONReads())
        }
 
diff --git a/io/gcs_integration_test.go b/io/gocloud/gcs_integration_test.go
similarity index 97%
rename from io/gcs_integration_test.go
rename to io/gocloud/gcs_integration_test.go
index f2c0556c..0b86628d 100644
--- a/io/gcs_integration_test.go
+++ b/io/gocloud/gcs_integration_test.go
@@ -17,7 +17,7 @@
 
 //go:build integration
 
-package io_test
+package gocloud_test
 
 import (
        "bytes"
@@ -32,6 +32,7 @@ import (
        "github.com/apache/iceberg-go/catalog"
        sqlcat "github.com/apache/iceberg-go/catalog/sql"
        "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/stretchr/testify/suite"
        "github.com/uptrace/bun/driver/sqliteshim"
 )
@@ -114,7 +115,7 @@ func (s *GCSIOTestSuite) TestGCSWarehouse() {
                "type":            "sql",
                "warehouse":       fmt.Sprintf("gs://%s/iceberg/", 
gcsBucketName),
                io.GCSEndpoint:    fmt.Sprintf("http://%s/";, gcsEndpoint),
-               io.GCSUseJsonAPI:  "true",
+               io.GCSUseJSONAPI:  "true",
        }
 
        cat, err := catalog.Load(context.Background(), "default", properties)
diff --git a/io/gocloud/mem_test.go b/io/gocloud/mem_test.go
new file mode 100644
index 00000000..a7d536ba
--- /dev/null
+++ b/io/gocloud/mem_test.go
@@ -0,0 +1,133 @@
+// 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 gocloud_test
+
+import (
+       "context"
+       "io"
+       "testing"
+
+       icebergio "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestMemIO_BasicOperations(t *testing.T) {
+       ctx := context.Background()
+
+       memIO, err := icebergio.LoadFS(ctx, map[string]string{}, 
"mem://bucket/")
+       require.NoError(t, err)
+       require.NotNil(t, memIO)
+
+       writeIO, ok := memIO.(icebergio.WriteFileIO)
+       require.True(t, ok, "mem IO should implement WriteFileIO")
+
+       testData := []byte("Hello, Iceberg!")
+       err = writeIO.WriteFile("test-file.txt", testData)
+       require.NoError(t, err)
+
+       file, err := memIO.Open("test-file.txt")
+       require.NoError(t, err)
+       defer file.Close()
+
+       content, err := io.ReadAll(file)
+       require.NoError(t, err)
+       assert.Equal(t, testData, content)
+
+       err = memIO.Remove("test-file.txt")
+       require.NoError(t, err)
+
+       _, err = memIO.Open("test-file.txt")
+       assert.Error(t, err)
+}
+
+func TestMemIO_Create(t *testing.T) {
+       ctx := context.Background()
+
+       memIO, err := icebergio.LoadFS(ctx, map[string]string{}, 
"mem://bucket/")
+       require.NoError(t, err)
+
+       writeIO := memIO.(icebergio.WriteFileIO)
+
+       writer, err := writeIO.Create("created-file.txt")
+       require.NoError(t, err)
+       require.NotNil(t, writer)
+
+       testData := []byte("Data written via Create")
+       n, err := writer.Write(testData)
+       require.NoError(t, err)
+       assert.Equal(t, len(testData), n)
+
+       err = writer.Close()
+       require.NoError(t, err)
+
+       file, err := memIO.Open("created-file.txt")
+       require.NoError(t, err)
+       defer file.Close()
+
+       content, err := io.ReadAll(file)
+       require.NoError(t, err)
+       assert.Equal(t, testData, content)
+}
+
+func TestMemIO_MultipleFiles(t *testing.T) {
+       ctx := context.Background()
+
+       memIO, err := icebergio.LoadFS(ctx, map[string]string{}, 
"mem://bucket/")
+       require.NoError(t, err)
+
+       writeIO := memIO.(icebergio.WriteFileIO)
+
+       files := map[string][]byte{
+               "file1.txt": []byte("Content of file 1"),
+               "file2.txt": []byte("Content of file 2"),
+               "file3.txt": []byte("Content of file 3"),
+       }
+
+       for name, content := range files {
+               err := writeIO.WriteFile(name, content)
+               require.NoError(t, err)
+       }
+
+       for name, expectedContent := range files {
+               file, err := memIO.Open(name)
+               require.NoError(t, err)
+
+               content, err := io.ReadAll(file)
+               require.NoError(t, err)
+               assert.Equal(t, expectedContent, content)
+
+               err = file.Close()
+               require.NoError(t, err)
+       }
+
+       err = memIO.Remove("file2.txt")
+       require.NoError(t, err)
+
+       _, err = memIO.Open("file2.txt")
+       assert.Error(t, err)
+
+       file1, err := memIO.Open("file1.txt")
+       require.NoError(t, err)
+       file1.Close()
+
+       file3, err := memIO.Open("file3.txt")
+       require.NoError(t, err)
+       file3.Close()
+}
diff --git a/io/gocloud/register.go b/io/gocloud/register.go
new file mode 100644
index 00000000..7c2b4858
--- /dev/null
+++ b/io/gocloud/register.go
@@ -0,0 +1,85 @@
+// 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 gocloud
+
+import (
+       "context"
+       "net/url"
+
+       icebergio "github.com/apache/iceberg-go/io"
+       "gocloud.dev/blob/memblob"
+)
+
+func init() {
+       registerS3Schemes()
+       registerGCSScheme()
+       registerMemScheme()
+       registerAzureSchemes()
+}
+
+// registerS3Schemes registers S3-compatible storage schemes (s3, s3a, s3n).
+func registerS3Schemes() {
+       s3Factory := func(ctx context.Context, parsed *url.URL, props 
map[string]string) (icebergio.IO, error) {
+               bucket, err := createS3Bucket(ctx, parsed, props)
+               if err != nil {
+                       return nil, err
+               }
+
+               return createBlobFS(ctx, bucket, 
defaultKeyExtractor(parsed.Host)), nil
+       }
+       icebergio.Register("s3", s3Factory)
+       icebergio.Register("s3a", s3Factory)
+       icebergio.Register("s3n", s3Factory)
+}
+
+// registerGCSScheme registers the Google Cloud Storage scheme (gs).
+func registerGCSScheme() {
+       icebergio.Register("gs", func(ctx context.Context, parsed *url.URL, 
props map[string]string) (icebergio.IO, error) {
+               bucket, err := createGCSBucket(ctx, parsed, props)
+               if err != nil {
+                       return nil, err
+               }
+
+               return createBlobFS(ctx, bucket, 
defaultKeyExtractor(parsed.Host)), nil
+       })
+}
+
+// registerMemScheme registers the in-memory blob storage scheme (mem).
+func registerMemScheme() {
+       icebergio.Register("mem", func(ctx context.Context, parsed *url.URL, 
props map[string]string) (icebergio.IO, error) {
+               bucket := memblob.OpenBucket(nil)
+
+               return createBlobFS(ctx, bucket, 
defaultKeyExtractor(parsed.Host)), nil
+       })
+}
+
+// registerAzureSchemes registers Azure Data Lake Storage schemes (abfs, 
abfss, wasb, wasbs).
+func registerAzureSchemes() {
+       azureFactory := func(ctx context.Context, parsed *url.URL, props 
map[string]string) (icebergio.IO, error) {
+               bucket, err := createAzureBucket(ctx, parsed, props)
+               if err != nil {
+                       return nil, err
+               }
+
+               return createBlobFS(ctx, bucket, adlsKeyExtractor()), nil
+       }
+       icebergio.Register("abfs", azureFactory)
+       icebergio.Register("abfss", azureFactory)
+       icebergio.Register("wasb", azureFactory)
+       icebergio.Register("wasbs", azureFactory)
+}
diff --git a/io/s3.go b/io/gocloud/s3.go
similarity index 79%
rename from io/s3.go
rename to io/gocloud/s3.go
index 435962ab..52d7e60c 100644
--- a/io/s3.go
+++ b/io/gocloud/s3.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import (
        "context"
@@ -27,6 +27,7 @@ import (
        "slices"
        "strconv"
 
+       "github.com/apache/iceberg-go/io"
        "github.com/apache/iceberg-go/utils"
        "github.com/aws/aws-sdk-go-v2/aws"
        awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
@@ -38,22 +39,8 @@ import (
        "gocloud.dev/blob/s3blob"
 )
 
-// Constants for S3 configuration options
-const (
-       S3Region                 = "s3.region"
-       S3SessionToken           = "s3.session-token"
-       S3SecretAccessKey        = "s3.secret-access-key"
-       S3AccessKeyID            = "s3.access-key-id"
-       S3EndpointURL            = "s3.endpoint"
-       S3ProxyURI               = "s3.proxy-uri"
-       S3ConnectTimeout         = "s3.connect-timeout"
-       S3SignerUri              = "s3.signer.uri"
-       S3RemoteSigningEnabled   = "s3.remote-signing-enabled"
-       S3ForceVirtualAddressing = "s3.force-virtual-addressing"
-)
-
 var unsupportedS3Props = []string{
-       S3ConnectTimeout,
+       io.S3ConnectTimeout,
 }
 
 // ParseAWSConfig parses S3 properties and returns a configuration.
@@ -66,7 +53,7 @@ func ParseAWSConfig(ctx context.Context, props 
map[string]string) (*aws.Config,
        }
 
        // Remote S3 request signing is not implemented yet.
-       if v, ok := props[S3RemoteSigningEnabled]; ok {
+       if v, ok := props[io.S3RemoteSigningEnabled]; ok {
                if enabled, err := strconv.ParseBool(v); err == nil && enabled {
                        return nil, errors.New("remote S3 request signing is 
not supported")
                }
@@ -79,20 +66,20 @@ func ParseAWSConfig(ctx context.Context, props 
map[string]string) (*aws.Config,
                        &bearer.StaticTokenProvider{Token: bearer.Token{Value: 
tok}}))
        }
 
-       if region, ok := props[S3Region]; ok {
+       if region, ok := props[io.S3Region]; ok {
                opts = append(opts, config.WithRegion(region))
        } else if region, ok := props["client.region"]; ok {
                opts = append(opts, config.WithRegion(region))
        }
 
-       accessKey, secretAccessKey := props[S3AccessKeyID], 
props[S3SecretAccessKey]
-       token := props[S3SessionToken]
+       accessKey, secretAccessKey := props[io.S3AccessKeyID], 
props[io.S3SecretAccessKey]
+       token := props[io.S3SessionToken]
        if accessKey != "" || secretAccessKey != "" || token != "" {
                opts = append(opts, 
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
-                       props[S3AccessKeyID], props[S3SecretAccessKey], 
props[S3SessionToken])))
+                       props[io.S3AccessKeyID], props[io.S3SecretAccessKey], 
props[io.S3SessionToken])))
        }
 
-       if proxy, ok := props[S3ProxyURI]; ok {
+       if proxy, ok := props[io.S3ProxyURI]; ok {
                proxyURL, err := url.Parse(proxy)
                if err != nil {
                        return nil, fmt.Errorf("invalid s3 proxy url '%s'", 
proxy)
@@ -129,13 +116,13 @@ func createS3Bucket(ctx context.Context, parsed *url.URL, 
props map[string]strin
                }
        }
 
-       endpoint, ok := props[S3EndpointURL]
+       endpoint, ok := props[io.S3EndpointURL]
        if !ok {
                endpoint = os.Getenv("AWS_S3_ENDPOINT")
        }
 
        usePathStyle := true
-       if forceVirtual, ok := props[S3ForceVirtualAddressing]; ok {
+       if forceVirtual, ok := props[io.S3ForceVirtualAddressing]; ok {
                if cfgForceVirtual, err := strconv.ParseBool(forceVirtual); err 
== nil {
                        usePathStyle = !cfgForceVirtual
                }
diff --git a/io/s3_integration_test.go b/io/gocloud/s3_integration_test.go
similarity index 98%
rename from io/s3_integration_test.go
rename to io/gocloud/s3_integration_test.go
index 0e51ad02..c3197edd 100644
--- a/io/s3_integration_test.go
+++ b/io/gocloud/s3_integration_test.go
@@ -17,7 +17,7 @@
 
 //go:build integration
 
-package io_test
+package gocloud_test
 
 import (
        "context"
@@ -29,6 +29,7 @@ import (
        "github.com/apache/iceberg-go/catalog"
        sqlcat "github.com/apache/iceberg-go/catalog/sql"
        "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/stretchr/testify/require"
        "github.com/uptrace/bun/driver/sqliteshim"
 )
diff --git a/io/s3_test.go b/io/gocloud/s3_test.go
similarity index 83%
rename from io/s3_test.go
rename to io/gocloud/s3_test.go
index a7ac95aa..73d744de 100644
--- a/io/s3_test.go
+++ b/io/gocloud/s3_test.go
@@ -15,12 +15,13 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import (
        "context"
        "testing"
 
+       "github.com/apache/iceberg-go/io"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 )
@@ -32,8 +33,8 @@ func TestParseAWSConfigRemoteSigningEnabled(t *testing.T) {
                t.Parallel()
 
                _, err := ParseAWSConfig(context.Background(), 
map[string]string{
-                       S3SignerUri:            "https://signer.example.com";,
-                       S3RemoteSigningEnabled: "true",
+                       io.S3SignerURI:            "https://signer.example.com";,
+                       io.S3RemoteSigningEnabled: "true",
                })
                require.ErrorContains(t, err, "remote S3 request signing is not 
supported")
        })
@@ -42,9 +43,9 @@ func TestParseAWSConfigRemoteSigningEnabled(t *testing.T) {
                t.Parallel()
 
                _, err := ParseAWSConfig(context.Background(), 
map[string]string{
-                       S3SignerUri:            "https://signer.example.com";,
-                       S3RemoteSigningEnabled: "false",
-                       S3Region:               "us-east-1",
+                       io.S3SignerURI:            "https://signer.example.com";,
+                       io.S3RemoteSigningEnabled: "false",
+                       io.S3Region:               "us-east-1",
                })
                require.NoError(t, err)
        })
@@ -53,8 +54,8 @@ func TestParseAWSConfigRemoteSigningEnabled(t *testing.T) {
                t.Parallel()
 
                _, err := ParseAWSConfig(context.Background(), 
map[string]string{
-                       S3SignerUri: "https://signer.example.com";,
-                       S3Region:    "us-west-2",
+                       io.S3SignerURI: "https://signer.example.com";,
+                       io.S3Region:    "us-west-2",
                })
                require.NoError(t, err)
        })
@@ -63,7 +64,7 @@ func TestParseAWSConfigRemoteSigningEnabled(t *testing.T) {
                t.Parallel()
 
                _, err := ParseAWSConfig(context.Background(), 
map[string]string{
-                       S3RemoteSigningEnabled: "true",
+                       io.S3RemoteSigningEnabled: "true",
                })
                require.ErrorContains(t, err, "remote S3 request signing is not 
supported")
        })
@@ -72,7 +73,7 @@ func TestParseAWSConfigRemoteSigningEnabled(t *testing.T) {
                t.Parallel()
 
                cfg, err := ParseAWSConfig(context.Background(), 
map[string]string{
-                       S3Region: "eu-west-1",
+                       io.S3Region: "eu-west-1",
                })
                require.NoError(t, err)
                assert.Equal(t, "eu-west-1", cfg.Region)
@@ -83,7 +84,7 @@ func TestParseAWSConfigUnsupportedProperty(t *testing.T) {
        t.Parallel()
 
        _, err := ParseAWSConfig(context.Background(), map[string]string{
-               S3ConnectTimeout: "5000",
+               io.S3ConnectTimeout: "5000",
        })
        require.ErrorContains(t, err, "unsupported S3 property")
 }
diff --git a/io/utils.go b/io/gocloud/utils.go
similarity index 98%
rename from io/utils.go
rename to io/gocloud/utils.go
index 5c7f3214..ba4ed9c6 100644
--- a/io/utils.go
+++ b/io/gocloud/utils.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package io
+package gocloud
 
 import "strings"
 
diff --git a/io/io.go b/io/io.go
index 447b515b..1eac931f 100644
--- a/io/io.go
+++ b/io/io.go
@@ -15,6 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
+// Package io provides an interface for IO implementations along with
+// a registry for registering IO implementations for different URI schemes.
+//
+// Subpackages of this package provide implementations for cloud storage 
providers
+// which will register themselves if imported. For instance, adding the 
following
+// import:
+//
+//     import _ "github.com/apache/iceberg-go/io/gocloud"
+//
+// Will register cloud storage implementations for S3, GCS, Azure, and 
in-memory
+// blob storage. The local filesystem (file:// and empty scheme) is registered
+// by default.
 package io
 
 import (
@@ -23,11 +35,7 @@ import (
        "fmt"
        "io"
        "io/fs"
-       "net/url"
        "strings"
-
-       "gocloud.dev/blob"
-       "gocloud.dev/blob/memblob"
 )
 
 // IO is an interface to a hierarchical file system.
@@ -194,6 +202,8 @@ func (f ioFS) Remove(name string) error {
 }
 
 var (
+       ErrIOSchemeNotFound = errors.New("io scheme not registered")
+
        errMissingReadDir  = errors.New("fs.File directory missing ReadDir 
method")
        errMissingSeek     = errors.New("fs.File missing Seek method")
        errMissingReadAt   = errors.New("fs.File missing ReadAt")
@@ -235,60 +245,24 @@ func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) 
{
        return d.ReadDir(count)
 }
 
-func inferFileIOFromSchema(ctx context.Context, path string, props 
map[string]string) (IO, error) {
-       parsed, err := url.Parse(path)
-       if err != nil {
-               return nil, err
-       }
-       var bucket *blob.Bucket
-       var keyExtractor KeyExtractor
-
-       switch parsed.Scheme {
-       case "s3", "s3a", "s3n":
-               bucket, err = createS3Bucket(ctx, parsed, props)
-               if err != nil {
-                       return nil, err
-               }
-               keyExtractor = defaultKeyExtractor(parsed.Host)
-       case "gs":
-               bucket, err = createGCSBucket(ctx, parsed, props)
-               if err != nil {
-                       return nil, err
-               }
-               keyExtractor = defaultKeyExtractor(parsed.Host)
-       case "mem":
-               // memblob doesn't use the URL host or path
-               bucket = memblob.OpenBucket(nil)
-               keyExtractor = defaultKeyExtractor(parsed.Host)
-       case "file", "":
-               return LocalFS{}, nil
-       case "abfs", "abfss", "wasb", "wasbs":
-               bucket, err = createAzureBucket(ctx, parsed, props)
-               if err != nil {
-                       return nil, err
-               }
-               keyExtractor = adlsKeyExtractor()
-       default:
-               return nil, fmt.Errorf("IO for file '%s' not implemented", path)
-       }
-
-       return createBlobFS(ctx, bucket, keyExtractor), nil
-}
-
 // LoadFS takes a map of properties and an optional URI location
-// and attempts to infer an IO object from it.
+// and attempts to infer an IO object from it using the registered
+// scheme factories.
+//
+// The scheme is extracted from the location URI and used to look up
+// the appropriate factory from the registry. The local filesystem
+// (file:// or empty scheme) is registered by default.
 //
-// A schema of "file://" or an empty string will result in a LocalFS
-// implementation. Otherwise this will return an error if the schema
-// does not yet have an implementation here.
+// Additional schemes can be registered by importing subpackages.
+// For S3, GCS, Azure and in-memory support, import:
 //
-// Currently local, S3, GCS, and In-Memory FSs are implemented.
+//     import _ "github.com/apache/iceberg-go/io/gocloud"
 func LoadFS(ctx context.Context, props map[string]string, location string) 
(IO, error) {
        if location == "" {
                location = props["warehouse"]
        }
 
-       iofs, err := inferFileIOFromSchema(ctx, location, props)
+       iofs, err := inferFileIOFromScheme(ctx, location, props)
        if err != nil {
                return nil, err
        }
diff --git a/io/registry.go b/io/registry.go
new file mode 100644
index 00000000..f18385fa
--- /dev/null
+++ b/io/registry.go
@@ -0,0 +1,93 @@
+// 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 io
+
+import (
+       "context"
+       "fmt"
+       "maps"
+       "net/url"
+       "slices"
+       "sync"
+)
+
+type registry map[string]SchemeFactory
+
+var (
+       regMutex        sync.RWMutex
+       defaultRegistry = registry{}
+)
+
+// SchemeFactory is a function that creates an IO implementation for a given 
URI and properties.
+type SchemeFactory func(ctx context.Context, parsed *url.URL, props 
map[string]string) (IO, error)
+
+// Register adds a new scheme factory to the registry. If the scheme is 
already registered, it will panic.
+func Register(scheme string, factory SchemeFactory) {
+       if factory == nil {
+               panic("io: Register factory is nil")
+       }
+
+       regMutex.Lock()
+       defer regMutex.Unlock()
+
+       if _, dup := defaultRegistry[scheme]; dup {
+               panic("io: Register called twice for scheme " + scheme)
+       }
+       defaultRegistry[scheme] = factory
+}
+
+// Unregister removes the requested scheme factory from the registry.
+func Unregister(scheme string) {
+       regMutex.Lock()
+       defer regMutex.Unlock()
+       delete(defaultRegistry, scheme)
+}
+
+// GetRegisteredSchemes returns the list of registered scheme names.
+func GetRegisteredSchemes() []string {
+       regMutex.RLock()
+       defer regMutex.RUnlock()
+
+       return slices.Collect(maps.Keys(defaultRegistry))
+}
+
+func init() {
+       // Register local filesystem schemes
+       localFSFactory := func(ctx context.Context, parsed *url.URL, props 
map[string]string) (IO, error) {
+               return LocalFS{}, nil
+       }
+       Register("file", localFSFactory)
+       Register("", localFSFactory)
+}
+
+func inferFileIOFromScheme(ctx context.Context, path string, props 
map[string]string) (IO, error) {
+       parsed, err := url.Parse(path)
+       if err != nil {
+               return nil, err
+       }
+
+       regMutex.RLock()
+       factory, ok := defaultRegistry[parsed.Scheme]
+       regMutex.RUnlock()
+
+       if !ok {
+               return nil, fmt.Errorf("%w for path %q (scheme: %s)", 
ErrIOSchemeNotFound, path, parsed.Scheme)
+       }
+
+       return factory(ctx, parsed, props)
+}
diff --git a/io/registry_test.go b/io/registry_test.go
new file mode 100644
index 00000000..bb257daa
--- /dev/null
+++ b/io/registry_test.go
@@ -0,0 +1,175 @@
+// 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 io_test
+
+import (
+       "context"
+       "net/url"
+       "testing"
+
+       "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestIORegistry(t *testing.T) {
+       ctx := context.Background()
+
+       assert.ElementsMatch(t, []string{
+               "file",
+               "",
+               "s3",
+               "s3a",
+               "s3n",
+               "gs",
+               "mem",
+               "abfs",
+               "abfss",
+               "wasb",
+               "wasbs",
+       }, io.GetRegisteredSchemes())
+
+       customFactoryCalled := false
+       io.Register("custom", func(ctx context.Context, parsed *url.URL, props 
map[string]string) (io.IO, error) {
+               customFactoryCalled = true
+               assert.Equal(t, "custom", parsed.Scheme)
+               assert.Equal(t, "bucket", parsed.Host)
+
+               return io.LocalFS{}, nil
+       })
+
+       assert.ElementsMatch(t, []string{
+               "file",
+               "",
+               "s3",
+               "s3a",
+               "s3n",
+               "gs",
+               "mem",
+               "abfs",
+               "abfss",
+               "wasb",
+               "wasbs",
+               "custom",
+       }, io.GetRegisteredSchemes())
+
+       customIO, err := io.LoadFS(ctx, map[string]string{}, 
"custom://bucket/path")
+       assert.NoError(t, err)
+       assert.NotNil(t, customIO)
+       assert.True(t, customFactoryCalled)
+
+       io.Unregister("custom")
+       assert.ElementsMatch(t, []string{
+               "file",
+               "",
+               "s3",
+               "s3a",
+               "s3n",
+               "gs",
+               "mem",
+               "abfs",
+               "abfss",
+               "wasb",
+               "wasbs",
+       }, io.GetRegisteredSchemes())
+
+       _, err = io.LoadFS(ctx, map[string]string{}, "custom://bucket/path")
+       assert.Error(t, err)
+       assert.ErrorIs(t, err, io.ErrIOSchemeNotFound)
+}
+
+func TestRegistryPanic(t *testing.T) {
+       assert.PanicsWithValue(t, "io: Register factory is nil", func() {
+               io.Register("invalid", nil)
+       })
+}
+
+func TestRegisterDuplicatePanic(t *testing.T) {
+       dummyFactory := func(ctx context.Context, parsed *url.URL, props 
map[string]string) (io.IO, error) {
+               return io.LocalFS{}, nil
+       }
+
+       io.Register("test-duplicate", dummyFactory)
+       defer io.Unregister("test-duplicate")
+
+       assert.PanicsWithValue(t, "io: Register called twice for scheme 
test-duplicate", func() {
+               io.Register("test-duplicate", dummyFactory)
+       }, "Attempting to register the same scheme twice should panic")
+}
+
+func TestLoadFS(t *testing.T) {
+       ctx := context.Background()
+
+       tests := []struct {
+               name        string
+               location    string
+               expectError bool
+       }{
+               {
+                       name:        "file scheme",
+                       location:    "file:///tmp/test",
+                       expectError: false,
+               },
+               {
+                       name:        "empty scheme",
+                       location:    "/tmp/test",
+                       expectError: false,
+               },
+               {
+                       name:        "mem scheme",
+                       location:    "mem://bucket/path",
+                       expectError: false,
+               },
+               {
+                       name:        "s3 scheme",
+                       location:    "s3://bucket/path",
+                       expectError: false,
+               },
+               {
+                       name:        "unsupported scheme",
+                       location:    "unsupported://bucket/path",
+                       expectError: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       iofs, err := io.LoadFS(ctx, map[string]string{}, 
tt.location)
+                       if tt.expectError {
+                               assert.Error(t, err)
+                               assert.ErrorIs(t, err, io.ErrIOSchemeNotFound)
+                       } else {
+                               require.NoError(t, err)
+                               assert.NotNil(t, iofs)
+                       }
+               })
+       }
+}
+
+func TestLoadFSWithWarehouse(t *testing.T) {
+       ctx := context.Background()
+
+       // Test with warehouse property
+       iofs, err := io.LoadFS(ctx, map[string]string{
+               "warehouse": "file:///tmp/warehouse",
+       }, "")
+       require.NoError(t, err)
+       assert.NotNil(t, iofs)
+       assert.IsType(t, io.LocalFS{}, iofs)
+}
diff --git a/table/orphan_cleanup_integration_test.go 
b/table/orphan_cleanup_integration_test.go
index bc4d3883..cce3fd0b 100644
--- a/table/orphan_cleanup_integration_test.go
+++ b/table/orphan_cleanup_integration_test.go
@@ -34,6 +34,7 @@ import (
        "github.com/apache/iceberg-go/catalog"
        "github.com/apache/iceberg-go/catalog/rest"
        "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
        "github.com/stretchr/testify/suite"
 )
diff --git a/table/scanner_test.go b/table/scanner_test.go
index f3dadb06..6554c134 100644
--- a/table/scanner_test.go
+++ b/table/scanner_test.go
@@ -38,6 +38,7 @@ import (
        "github.com/apache/iceberg-go/catalog/rest"
        "github.com/apache/iceberg-go/internal/recipe"
        "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
        "github.com/stretchr/testify/require"
        "github.com/stretchr/testify/suite"
diff --git a/table/snapshot_producers_test.go b/table/snapshot_producers_test.go
index 9c008886..88e981ed 100644
--- a/table/snapshot_producers_test.go
+++ b/table/snapshot_producers_test.go
@@ -31,6 +31,7 @@ import (
        "github.com/apache/iceberg-go"
        "github.com/apache/iceberg-go/internal"
        iceio "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/stretchr/testify/require"
 )
 
diff --git a/table/transaction_test.go b/table/transaction_test.go
index 1f64adf1..db06d64d 100644
--- a/table/transaction_test.go
+++ b/table/transaction_test.go
@@ -35,6 +35,7 @@ import (
        "github.com/apache/iceberg-go/catalog/rest"
        "github.com/apache/iceberg-go/internal/recipe"
        iceio "github.com/apache/iceberg-go/io"
+       _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
        "github.com/stretchr/testify/suite"
        "github.com/testcontainers/testcontainers-go/modules/compose"


Reply via email to