Repository: apache/iceberg-go
Pull request #313: feat: add support for azure blob with connection string/sas token/account key
Author: login: xuhui-lu
Labels: name: INFRA
Created: 2025-02-22T17:02:34Z | Merged: 2025-04-02T17:37:52Z

It is a PR to add support for Azure blob, and it is refactored based on this PR: https://github.com/apache/iceberg-go/pull/278


Diff:
diff --git a/.gitignore b/.gitignore
index 99c39760d..60c835e73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,3 +56,6 @@ dev/warehouse
 /dev/release/apache-rat-*.jar
 /dev/release/filtered_rat.txt
 /dev/release/rat.xml
+
+# exec file generated by build
+cmd/iceberg/iceberg
diff --git a/README.md b/README.md
index 2bfe7bb04..1482ceaac 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ $ cd iceberg-go/cmd/iceberg && go build .
 | :------------------: | :-------: |
 | S3                   |    X      |
 | Google Cloud Storage |    X      |
-| Azure Blob Storage   |           |
+| Azure Blob Storage   |    X      |
 | Local Filesystem     |    X      |
 
 ### Metadata
diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml
index 0ca179ae4..1d3c3b1d5 100644
--- a/dev/docker-compose.yml
+++ b/dev/docker-compose.yml
@@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 11000:11000
+    command: ["azurite-blob", "--loose", "--blobHost", "0.0.0.0" ,"--blobPort", "11000"]
   mc:
     depends_on:
       - minio
diff --git a/go.mod b/go.mod
index 7838ffe7b..13bd769d7 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ go 1.23.0
 toolchain go1.23.6
 
 require (
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
 	github.com/apache/arrow-go/v18 v18.2.1-0.20250325140533-276892c275de
 	github.com/aws/aws-sdk-go-v2 v1.36.3
 	github.com/aws/aws-sdk-go-v2/config v1.29.9
@@ -62,6 +63,12 @@ require (
 	cloud.google.com/go/iam v1.4.2 // indirect
 	cloud.google.com/go/monitoring v1.24.0 // indirect
 	cloud.google.com/go/storage v1.51.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
+	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
@@ -98,6 +105,7 @@ require (
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/goccy/go-yaml v1.16.0 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/golang/snappy v1.0.0 // indirect
 	github.com/google/flatbuffers v25.2.10+incompatible // indirect
@@ -112,6 +120,7 @@ require (
 	github.com/klauspost/asmfmt v1.3.2 // indirect
 	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/lithammer/fuzzysearch v1.1.8 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
@@ -124,6 +133,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/pierrec/lz4/v4 v4.1.22 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
diff --git a/go.sum b/go.sum
index fd354184b..bf87eae75 100644
--- a/go.sum
+++ b/go.sum
@@ -29,6 +29,22 @@ cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDl
 cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
 cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE=
 cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E=
+github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
+github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
@@ -51,8 +67,6 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
 github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
 github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
 github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
-github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q=
-github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc=
 github.com/apache/arrow-go/v18 v18.2.1-0.20250325140533-276892c275de h1:wklDQWXjXSjWDoYXJvlPKii9nx6nXC8dEY/a4XRQOvI=
 github.com/apache/arrow-go/v18 v18.2.1-0.20250325140533-276892c275de/go.mod h1:EOkJNffq8UnNiioDTUIuynsY5mDoSojZHQZ5ELgGnWM=
 github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
@@ -146,6 +160,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/goccy/go-yaml v1.16.0 h1:d7m1G7A0t+logajVtklHfDYJs2Et9g3gHwdBNNFou0w=
 github.com/goccy/go-yaml v1.16.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
@@ -225,6 +241,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
@@ -253,6 +271,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
 github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
 github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -496,8 +516,9 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/io/azure.go b/io/azure.go
new file mode 100644
index 000000000..43ba54a6b
--- /dev/null
+++ b/io/azure.go
@@ -0,0 +1,113 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
+
+	var client *container.Client
+
+	if accountName == "" {
+		return nil, errors.New("account name is required for azure bucket")
+	}
+
+	if accountKey, ok := props[AdlsSharedKeyAccountKey]; ok {
+		svcURL, err := azureblob.NewServiceURL(&azureblob.ServiceURLOptions{
+			AccountName:   accountName,
+			Protocol:      protocol,
+			StorageDomain: endpoint,
+		})
+		if err != nil {
+			return nil, err
+		}
+		containerURL, err := url.JoinPath(string(svcURL), parsed.Host)
+		if err != nil {
+			return nil, err
+		}
+		sharedKeyCred, err := azblob.NewSharedKeyCredential(accountName, accountKey)
+		if err != nil {
+			return nil, fmt.Errorf("failed azblob.NewSharedKeyCredential: %w", err)
+		}
+
+		client, err = container.NewClientWithSharedKeyCredential(containerURL, sharedKeyCred, nil)
+		if err != nil {
+			return nil, fmt.Errorf("failed container.NewClientWithSharedKeyCredential: %w", err)
+		}
+	} else if sasToken, ok := adlsSasTokens[accountName]; ok {
+		svcURL, err := azureblob.NewServiceURL(&azureblob.ServiceURLOptions{
+			AccountName:   accountName,
+			SASToken:      sasToken,
+			Protocol:      protocol,
+			StorageDomain: endpoint,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		containerURL, err := url.JoinPath(string(svcURL), parsed.Host)
+		if err != nil {
+			return nil, err
+		}
+
+		client, err = container.NewClientWithNoCredential(containerURL, nil)
+		if err != nil {
+			return nil, fmt.Errorf("failed container.NewClientWithNoCredential: %w", err)
+		}
+	} else if connectionString, ok := adlsConnectionStrings[accountName]; ok {
+		var err error
+		client, err = container.NewClientFromConnectionString(connectionString, parsed.Host, nil)
+		if err != nil {
+			return nil, fmt.Errorf("failed container.NewClientFromConnectionString: %w", err)
+		}
+	}
+
+	return azureblob.OpenBucket(ctx, client, nil)
+}
diff --git a/io/azure_integration_test.go b/io/azure_integration_test.go
new file mode 100644
index 000000000..08c0eb438
--- /dev/null
+++ b/io/azure_integration_test.go
@@ -0,0 +1,160 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName              = "devstoreaccount1"
+	accountKey               = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint                 = "127.0.0.1:11000"
+	protocol                 = "http"
+	containerName            = "warehouse"
+	connectionStringTemplate = "DefaultEndpointsProtocol=%s;AccountName=%s;AccountKey=%s;BlobEndpoint=%s://%s/%s;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	s.Require().NoError(s.createContainerIfNotExist(containerName))
+}
+
+func (s *AzureBlobIOTestSuite) TestAzureBlobWarehouseKey() {
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+	properties := iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsSharedKeyAccountKey:  accountKey,
+		io.AdlsEndpoint:             endpoint,
+		io.AdlsProtocol:             protocol,
+	}
+
+	cat, err := catalog.Load(context.Background(), "default", properties)
+	s.Require().NoError(err)
+	s.Require().NotNil(cat)
+
+	c := cat.(*sqlcat.Catalog)
+	s.Require().NoError(c.CreateNamespace(s.ctx, catalog.ToIdentifier("iceberg-test-azure"), nil))
+
+	tbl, err := c.CreateTable(s.ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		iceberg.NewSchema(0, iceberg.NestedField{
+			Name: "id", Type: iceberg.PrimitiveTypes.Int32, Required: true, ID: 1,
+		}), catalog.WithLocation(fmt.Sprintf("abfs://%s/iceberg/%s", containerName, path)))
+	s.Require().NoError(err)
+	s.Require().NotNil(tbl)
+
+	tbl, err = c.LoadTable(s.ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		properties)
+	s.Require().NoError(err)
+	s.Require().NotNil(tbl)
+}
+
+func (s *AzureBlobIOTestSuite) TestAzuriteWarehouseConnectionString() {
+	connectionString := fmt.Sprintf(connectionStringTemplate, protocol, accountName, accountKey, protocol, endpoint, accountName)
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+	properties := iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsConnectionStringPrefix + accountName: connectionString,
+	}
+
+	cat, err := catalog.Load(context.Background(), "default", properties)
+	s.Require().NoError(err)
+	s.Require().NotNil(cat)
+
+	c := cat.(*sqlcat.Catalog)
+	s.Require().NoError(c.CreateNamespace(s.ctx, catalog.ToIdentifier("iceberg-test-azure"), nil))
+
+	tbl, err := c.CreateTable(s.ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		iceberg.NewSchema(0, iceberg.NestedField{
+			Name: "id", Type: iceberg.PrimitiveTypes.Int32, Required: true, ID: 1,
+		}), catalog.WithLocation(fmt.Sprintf("wasb://%s/iceberg/%s", containerName, path)))
+	s.Require().NoError(err)
+	s.Require().NotNil(tbl)
+
+	tbl, err = c.LoadTable(s.ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		properties)
+	s.Require().NoError(err)
+	s.Require().NotNil(tbl)
+}
+
+func (s *AzureBlobIOTestSuite) createContainerIfNotExist(containerName string) error {
+	svcURL, err := azureblob.NewServiceURL(&azureblob.ServiceURLOptions{
+		AccountName:   accountName,
+		Protocol:      protocol,
+		StorageDomain: endpoint,
+	})
+	if err != nil {
+		return err
+	}
+
+	sharedKeyCred, err := azblob.NewSharedKeyCredential(accountName, accountKey)
+	if err != nil {
+		return err
+	}
+
+	client, err := azblob.NewClientWithSharedKeyCredential(string(svcURL), sharedKeyCred, nil)
+	if err != nil {
+		return err
+	}
+
+	_, err = client.CreateContainer(s.ctx, containerName, nil)
+	if err != nil && !bloberror.HasCode(err, bloberror.ContainerAlreadyExists) {
+		return err
+	}
+
+	return nil
+}
+
+func TestAzureBlobIOIntegration(t *testing.T) {
+	suite.Run(t, new(AzureBlobIOTestSuite))
+}
diff --git a/io/io.go b/io/io.go
index 67318759e..523f7c575 100644
--- a/io/io.go
+++ b/io/io.go
@@ -258,6 +258,11 @@ func inferFileIOFromSchema(ctx context.Context, path string, props map[string]st
 		bucket = memblob.OpenBucket(nil)
 	case "file", "":
 		return LocalFS{}, nil
+	case "abfs", "abfss", "wasb", "wasbs":
+		bucket, err = createAzureBucket(ctx, parsed, props)
+		if err != nil {
+			return nil, err
+		}
 	default:
 		return nil, fmt.Errorf("IO for file '%s' not implemented", path)
 	}
diff --git a/io/utils.go b/io/utils.go
new file mode 100644
index 000000000..5c7f32142
--- /dev/null
+++ b/io/utils.go
@@ -0,0 +1,31 @@
+// 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 "strings"
+
+func propertiesWithPrefix(props map[string]string, prefix string) map[string]string {
+	result := map[string]string{}
+	for k, v := range props {
+		if strings.HasPrefix(k, prefix) {
+			result[strings.TrimPrefix(k, prefix)] = v
+		}
+	}
+
+	return result
+}


Reviews:
user: login: zeroshade
body: @kevinjqliu @Fokko do you know of any equivalent to running Minio that we could use via a docker image to test the ADLS integration?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-01T19:12:00Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T17:35:52Z
user: login: zeroshade
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-31T16:32:00Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T16:50:55Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T16:54:13Z
user: login: zeroshade
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T17:00:47Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T17:39:50Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T17:39:54Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T17:39:59Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T17:40:03Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T17:40:10Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T17:42:17Z
user: login: mrcnc
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T21:22:36Z
user: login: mrcnc
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T21:27:23Z
user: login: kevinjqliu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T17:47:03Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:13:43Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:14:51Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:22:48Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:29:40Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:30:14Z
user: login: kevinjqliu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T19:37:42Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T02:28:53Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T04:15:05Z
user: login: xuhui-lu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T04:15:15Z
user: login: kevinjqliu
body: LGTM i'll defer the rest to Matt :P 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:45:54Z
user: login: zeroshade
body: LGTM! Thanks for all the work here and getting this finished!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T17:37:43Z

Review comments:
diff_hunk: @@ -0,0 +1,104 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// Constants for Azure configuration options
+const (
+	ADLS_SAS_TOKEN_PREFIX         = "adls.sas-token."
+	ADLS_CONNECTION_STRING_PREFIX = "adls.connection-string."
+	ADLS_READ_BLOCK_SIZE          = "adls.read.block-size-bytes"
+	ADLS_WRITE_BLOCK_SIZE         = "adls.write.block-size-bytes"
+	ADLS_SHARED_KEY_ACCOUNT_NAME  = "adls.auth.shared-key.account.name"
+	ADLS_SHARED_KEY_ACCOUNT_KEY   = "adls.auth.shared-key.account.key"
path: io/azure.go
user: login: zeroshade
body: We don't tend to use the ALL_CAP_SNAKE_CASE for constants, instead we use the format `ADLSSasTokenPrefix` etc. please update
created_at: 2025-03-01T19:10:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,104 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// Constants for Azure configuration options
+const (
+	ADLS_SAS_TOKEN_PREFIX         = "adls.sas-token."
+	ADLS_CONNECTION_STRING_PREFIX = "adls.connection-string."
+	ADLS_READ_BLOCK_SIZE          = "adls.read.block-size-bytes"
+	ADLS_WRITE_BLOCK_SIZE         = "adls.write.block-size-bytes"
+	ADLS_SHARED_KEY_ACCOUNT_NAME  = "adls.auth.shared-key.account.name"
+	ADLS_SHARED_KEY_ACCOUNT_KEY   = "adls.auth.shared-key.account.key"
path: io/azure.go
user: login: xuhui-lu
body: updated.
created_at: 2025-03-08T17:35:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// Construct a Azure bucket from a URL
+func createAzureBucket(ctx context.Context, parsed *url.URL, props map[string]string) (*blob.Bucket, error) {
path: io/azure.go
user: login: zeroshade
body: use `iceberg.Properties` instead of `map[string]string`
created_at: 2025-03-31T15:53:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
path: io/azure.go
user: login: zeroshade
body: are there defaults we should use for these?
created_at: 2025-03-31T15:58:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
+
+	var client *container.Client
+
+	if accountName != "" {
path: io/azure.go
user: login: zeroshade
body: let's make this more readable by inverting this. 

```go
if accountName == "" {
    return nil, errors.New("account name is required for azure bucket")
}

// rest of code
```

Just makes it easier to read
created_at: 2025-03-31T16:11:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
+
+	var client *container.Client
+
+	if accountName != "" {
+		var sharedKeyCred *azblob.SharedKeyCredential
+		var err error
path: io/azure.go
user: login: zeroshade
body: i don't think you need to declare these separately outside of the conditions. 
created_at: 2025-03-31T16:12:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
+
+	var client *container.Client
+
+	if accountName != "" {
+		var sharedKeyCred *azblob.SharedKeyCredential
+		var err error
+
+		if accountKey, ok := props[AdlsSharedKeyAccountKey]; ok {
+			svcURL, err := azureblob.NewServiceURL(&azureblob.ServiceURLOptions{
+				AccountName:   accountName,
+				Protocol:      protocol,
+				StorageDomain: endpoint,
+			})
+			if err != nil {
+				return nil, err
+			}
+			containerURL, err := url.JoinPath(string(svcURL), parsed.Host)
+			if err != nil {
+				return nil, err
+			}
+			sharedKeyCred, err = azblob.NewSharedKeyCredential(accountName, accountKey)
+			if err != nil {
+				return nil, fmt.Errorf("failed azblob.NewSharedKeyCredential: %w", err)
+			}
+
+			client, err = container.NewClientWithSharedKeyCredential(containerURL, sharedKeyCred, nil)
+			if err != nil {
+				return nil, fmt.Errorf("failed container.NewClientWithSharedKeyCredential: %w", err)
+			}
+		} else if sasToken, ok := adlsSasTokens[accountName]; ok {
+			svcURL, err := azureblob.NewServiceURL(&azureblob.ServiceURLOptions{
+				AccountName:   accountName,
+				SASToken:      sasToken,
+				Protocol:      protocol,
+				StorageDomain: endpoint,
+			})
+			if err != nil {
+				return nil, err
+			}
+
+			containerURL, err := url.JoinPath(string(svcURL), parsed.Host)
+			if err != nil {
+				return nil, err
+			}
+
+			client, err = container.NewClientWithNoCredential(containerURL, nil)
+			if err != nil {
+				return nil, fmt.Errorf("failed container.NewClientWithNoCredential: %w", err)
+			}
+		} else if connectionString, ok := adlsConnectionStrings[accountName]; ok {
+			client, err = container.NewClientFromConnectionString(connectionString, parsed.Host, nil)
+			if err != nil {
+				return nil, fmt.Errorf("failed container.NewClientFromConnectionString: %w", err)
+			}
+		}
+
+		bucket, err := azureblob.OpenBucket(ctx, client, nil)
+		if err != nil {
+			return nil, err
+		}
+
+		return bucket, nil
path: io/azure.go
user: login: zeroshade
body: maybe just `return azureblob.OpenBucket(ctx, client, nil)` ?
created_at: 2025-03-31T16:14:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,148 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	err := createContainerIfNotExist(containerName)
+	s.NoError(err)
path: io/azure_integration_test.go
user: login: zeroshade
body: ```go
s.Require().NoError(createContainerIfNotExist(containerName)
```
created_at: 2025-03-31T16:15:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,148 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	err := createContainerIfNotExist(containerName)
+	s.NoError(err)
+}
+
+func (s *AzureBlobIOTestSuite) TestAzureBlobWarehouseKey() {
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+
+	cat, err := catalog.Load(context.Background(), "default", iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsSharedKeyAccountKey:  accountKey,
+		io.AdlsEndpoint:             endpoint,
+		io.AdlsProtocol:             protocol,
+	})
+	s.NoError(err)
path: io/azure_integration_test.go
user: login: zeroshade
body: use `s.Require().` for conditions like this where the test cannot continue if it fails.
created_at: 2025-03-31T16:16:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,148 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	err := createContainerIfNotExist(containerName)
+	s.NoError(err)
+}
+
+func (s *AzureBlobIOTestSuite) TestAzureBlobWarehouseKey() {
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+
+	cat, err := catalog.Load(context.Background(), "default", iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsSharedKeyAccountKey:  accountKey,
+		io.AdlsEndpoint:             endpoint,
+		io.AdlsProtocol:             protocol,
+	})
+	s.NoError(err)
+	s.NotNil(cat)
+
+	c := cat.(*sqlcat.Catalog)
+	s.NoError(c.CreateNamespace(s.ctx, catalog.ToIdentifier("iceberg-test-azure"), nil))
+
+	tbl, err := c.CreateTable(s.ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		iceberg.NewSchema(0, iceberg.NestedField{
+			Name: "id", Type: iceberg.PrimitiveTypes.Int32, Required: true, ID: 1,
+		}), catalog.WithLocation(fmt.Sprintf("abfs://%s/iceberg/%s", containerName, path)))
+	s.NoError(err)
+	s.NotNil(tbl)
path: io/azure_integration_test.go
user: login: zeroshade
body: should add a test here afterwards to load the table and ensure the data matches. Right now you're only testing that there's no errors when it tries to perform the write, there's no confirmation that it actually wrote the right things by attempting to read it back.
created_at: 2025-03-31T16:18:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,148 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	err := createContainerIfNotExist(containerName)
+	s.NoError(err)
+}
+
+func (s *AzureBlobIOTestSuite) TestAzureBlobWarehouseKey() {
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+
+	cat, err := catalog.Load(context.Background(), "default", iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsSharedKeyAccountKey:  accountKey,
+		io.AdlsEndpoint:             endpoint,
+		io.AdlsProtocol:             protocol,
+	})
+	s.NoError(err)
+	s.NotNil(cat)
+
+	c := cat.(*sqlcat.Catalog)
+	s.NoError(c.CreateNamespace(s.ctx, catalog.ToIdentifier("iceberg-test-azure"), nil))
+
+	tbl, err := c.CreateTable(s.ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		iceberg.NewSchema(0, iceberg.NestedField{
+			Name: "id", Type: iceberg.PrimitiveTypes.Int32, Required: true, ID: 1,
+		}), catalog.WithLocation(fmt.Sprintf("abfs://%s/iceberg/%s", containerName, path)))
+	s.NoError(err)
+	s.NotNil(tbl)
+}
+
+func (s *AzureBlobIOTestSuite) TestAzuriteWarehouseConnectionString() {
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+
+	cat, err := catalog.Load(context.Background(), "default", iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsConnectionStringPrefix + accountName: connectionString,
+	})
+	s.NoError(err)
+
+	s.NotNil(cat)
+
+	c := cat.(*sqlcat.Catalog)
+	ctx := context.Background()
+	s.NoError(c.CreateNamespace(ctx, catalog.ToIdentifier("iceberg-test-azure"), nil))
+
+	tbl, err := c.CreateTable(ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		iceberg.NewSchema(0, iceberg.NestedField{
+			Name: "id", Type: iceberg.PrimitiveTypes.Int32, Required: true, ID: 1,
+		}), catalog.WithLocation(fmt.Sprintf("abfs://%s/iceberg/%s", containerName, path)))
+	s.NoError(err)
+	s.NotNil(tbl)
path: io/azure_integration_test.go
user: login: zeroshade
body: same comment as above, add a LoadTable call
created_at: 2025-03-31T16:18:49Z
author_association: MEMBER
diff_hunk: @@ -258,6 +258,11 @@ func inferFileIOFromSchema(ctx context.Context, path string, props map[string]st
 		bucket = memblob.OpenBucket(nil)
 	case "file", "":
 		return LocalFS{}, nil
+	case "abfs", "abfss", "wasb", "wasbs":
path: io/io.go
user: login: zeroshade
body: do the different schemes mean anything important?
created_at: 2025-03-31T16:19:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+// 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 "strings"
+
+func propertiesWithPrefix(props map[string]string, prefix string) map[string]string {
path: io/utils.go
user: login: zeroshade
body: use `iceberg.Properties` instead please.
created_at: 2025-03-31T16:20:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
path: io/azure.go
user: login: xuhui-lu
body: ```
	if domain == "" {
		domain = "blob.core.windows.net"
	}
	protocol := opts.Protocol
	if protocol == "" {
		protocol = "https"
	} else if protocol != "http" && protocol != "https" {
		return "", fmt.Errorf("invalid protocol %q", protocol)
	}
```

The default value will be blob.core.windows.net and https which are returned from the sdk. We could also set it to be the default value on the iceberg-go side.
created_at: 2025-03-31T16:50:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// Construct a Azure bucket from a URL
+func createAzureBucket(ctx context.Context, parsed *url.URL, props map[string]string) (*blob.Bucket, error) {
path: io/azure.go
user: login: xuhui-lu
body: Adding reference to the current iceberg.Properties struct will cause import cycle, I think that is why we don't use it in gcs.go and s3.go. Should I just move it to the io package here?
created_at: 2025-03-31T16:54:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// Construct a Azure bucket from a URL
+func createAzureBucket(ctx context.Context, parsed *url.URL, props map[string]string) (*blob.Bucket, error) {
path: io/azure.go
user: login: zeroshade
body: ah, didn't realize that. that's fair. we shouldn't move it so that's fine then
created_at: 2025-03-31T17:00:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
+
+	var client *container.Client
+
+	if accountName != "" {
path: io/azure.go
user: login: xuhui-lu
body: done.
created_at: 2025-03-31T17:39:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
+
+	var client *container.Client
+
+	if accountName != "" {
+		var sharedKeyCred *azblob.SharedKeyCredential
+		var err error
path: io/azure.go
user: login: xuhui-lu
body: done.
created_at: 2025-03-31T17:39:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
+
+	var client *container.Client
+
+	if accountName != "" {
+		var sharedKeyCred *azblob.SharedKeyCredential
+		var err error
+
+		if accountKey, ok := props[AdlsSharedKeyAccountKey]; ok {
+			svcURL, err := azureblob.NewServiceURL(&azureblob.ServiceURLOptions{
+				AccountName:   accountName,
+				Protocol:      protocol,
+				StorageDomain: endpoint,
+			})
+			if err != nil {
+				return nil, err
+			}
+			containerURL, err := url.JoinPath(string(svcURL), parsed.Host)
+			if err != nil {
+				return nil, err
+			}
+			sharedKeyCred, err = azblob.NewSharedKeyCredential(accountName, accountKey)
+			if err != nil {
+				return nil, fmt.Errorf("failed azblob.NewSharedKeyCredential: %w", err)
+			}
+
+			client, err = container.NewClientWithSharedKeyCredential(containerURL, sharedKeyCred, nil)
+			if err != nil {
+				return nil, fmt.Errorf("failed container.NewClientWithSharedKeyCredential: %w", err)
+			}
+		} else if sasToken, ok := adlsSasTokens[accountName]; ok {
+			svcURL, err := azureblob.NewServiceURL(&azureblob.ServiceURLOptions{
+				AccountName:   accountName,
+				SASToken:      sasToken,
+				Protocol:      protocol,
+				StorageDomain: endpoint,
+			})
+			if err != nil {
+				return nil, err
+			}
+
+			containerURL, err := url.JoinPath(string(svcURL), parsed.Host)
+			if err != nil {
+				return nil, err
+			}
+
+			client, err = container.NewClientWithNoCredential(containerURL, nil)
+			if err != nil {
+				return nil, fmt.Errorf("failed container.NewClientWithNoCredential: %w", err)
+			}
+		} else if connectionString, ok := adlsConnectionStrings[accountName]; ok {
+			client, err = container.NewClientFromConnectionString(connectionString, parsed.Host, nil)
+			if err != nil {
+				return nil, fmt.Errorf("failed container.NewClientFromConnectionString: %w", err)
+			}
+		}
+
+		bucket, err := azureblob.OpenBucket(ctx, client, nil)
+		if err != nil {
+			return nil, err
+		}
+
+		return bucket, nil
path: io/azure.go
user: login: xuhui-lu
body: done.
created_at: 2025-03-31T17:39:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	err := createContainerIfNotExist(containerName)
+	s.NoError(err)
path: io/azure_integration_test.go
user: login: xuhui-lu
body: done.
created_at: 2025-03-31T17:40:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	err := createContainerIfNotExist(containerName)
+	s.NoError(err)
+}
+
+func (s *AzureBlobIOTestSuite) TestAzureBlobWarehouseKey() {
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+
+	cat, err := catalog.Load(context.Background(), "default", iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsSharedKeyAccountKey:  accountKey,
+		io.AdlsEndpoint:             endpoint,
+		io.AdlsProtocol:             protocol,
+	})
+	s.NoError(err)
path: io/azure_integration_test.go
user: login: xuhui-lu
body: updated as well.
created_at: 2025-03-31T17:40:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -258,6 +258,11 @@ func inferFileIOFromSchema(ctx context.Context, path string, props map[string]st
 		bucket = memblob.OpenBucket(nil)
 	case "file", "":
 		return LocalFS{}, nil
+	case "abfs", "abfss", "wasb", "wasbs":
path: io/io.go
user: login: xuhui-lu
body: hmm, just try to follow the definition in iceberg-jave:
```
  private static final Map<String, String> SCHEME_TO_FILE_IO =
      ImmutableMap.of(
          "s3", S3_FILE_IO_IMPL,
          "s3a", S3_FILE_IO_IMPL,
          "s3n", S3_FILE_IO_IMPL,
          "gs", GCS_FILE_IO_IMPL,
          "abfs", ADLS_FILE_IO_IMPL,
          "abfss", ADLS_FILE_IO_IMPL,
          "wasb", ADLS_FILE_IO_IMPL,
          "wasbs", ADLS_FILE_IO_IMPL);
```
created_at: 2025-03-31T17:42:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -258,6 +258,11 @@ func inferFileIOFromSchema(ctx context.Context, path string, props map[string]st
 		bucket = memblob.OpenBucket(nil)
 	case "file", "":
 		return LocalFS{}, nil
+	case "abfs", "abfss", "wasb", "wasbs":
path: io/io.go
user: login: mrcnc
body: > do the different schemes mean anything important?

The abfs URIs will use a different HTTP endpoint than wasb but they are addressing the same object.
created_at: 2025-03-31T21:22:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
path: io/azure.go
user: login: mrcnc
body: When `abfs` is the protocol, I believe `dfs.core.windows.net` should be the default according to [this](https://learn.microsoft.com/en-us/azure/storage/blobs/data-lake-storage-introduction-abfs-uri).   
created_at: 2025-03-31T21:27:22Z
author_association: NONE
diff_hunk: @@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 10010:10000
path: dev/docker-compose.yml
user: login: kevinjqliu
body: nit: use the same port for consistency

```suggestion
      - 10000:10000
```
created_at: 2025-04-01T17:45:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
path: io/azure_integration_test.go
user: login: kevinjqliu
body: nit: this seems to be using the constants above, can we use string interpolation here so that future changes can be made easier?
created_at: 2025-04-01T17:47:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 10010:10000
path: dev/docker-compose.yml
user: login: xuhui-lu
body: hmm, I use different port here since 10000 is already occupied by spark-iceberg here.
created_at: 2025-04-01T18:13:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
+)
+
+type AzureBlobIOTestSuite struct {
+	suite.Suite
+
+	ctx context.Context
+}
+
+func (s *AzureBlobIOTestSuite) SetupTest() {
+	s.ctx = context.Background()
+
+	err := createContainerIfNotExist(containerName)
+	s.NoError(err)
+}
+
+func (s *AzureBlobIOTestSuite) TestAzureBlobWarehouseKey() {
+	path := "iceberg-test-azure/test-table-azure"
+	containerName := "warehouse"
+
+	cat, err := catalog.Load(context.Background(), "default", iceberg.Properties{
+		"uri":                       ":memory:",
+		sqlcat.DriverKey:            sqliteshim.ShimName,
+		sqlcat.DialectKey:           string(sqlcat.SQLite),
+		"type":                      "sql",
+		io.AdlsSharedKeyAccountName: accountName,
+		io.AdlsSharedKeyAccountKey:  accountKey,
+		io.AdlsEndpoint:             endpoint,
+		io.AdlsProtocol:             protocol,
+	})
+	s.NoError(err)
+	s.NotNil(cat)
+
+	c := cat.(*sqlcat.Catalog)
+	s.NoError(c.CreateNamespace(s.ctx, catalog.ToIdentifier("iceberg-test-azure"), nil))
+
+	tbl, err := c.CreateTable(s.ctx,
+		catalog.ToIdentifier("iceberg-test-azure", "test-table-azure"),
+		iceberg.NewSchema(0, iceberg.NestedField{
+			Name: "id", Type: iceberg.PrimitiveTypes.Int32, Required: true, ID: 1,
+		}), catalog.WithLocation(fmt.Sprintf("abfs://%s/iceberg/%s", containerName, path)))
+	s.NoError(err)
+	s.NotNil(tbl)
path: io/azure_integration_test.go
user: login: xuhui-lu
body: done.
created_at: 2025-04-01T18:14:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+// 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"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"gocloud.dev/blob"
+	"gocloud.dev/blob/azureblob"
+)
+
+// 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"
+)
+
+// 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)
+
+	// Construct the client
+	accountName := props[AdlsSharedKeyAccountName]
+	endpoint := props[AdlsEndpoint]
+	protocol := props[AdlsProtocol]
path: io/azure.go
user: login: xuhui-lu
body: that is the case here. I'd like to make the user to set this value by themselves if necessary while our default for now could only support wasb actually.
created_at: 2025-04-01T18:22:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+// 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.
+
+//go:build integration
+
+package io_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/catalog"
+	sqlcat "github.com/apache/iceberg-go/catalog/sql"
+	"github.com/apache/iceberg-go/io"
+	"github.com/stretchr/testify/suite"
+	"github.com/uptrace/bun/driver/sqliteshim"
+	"gocloud.dev/blob/azureblob"
+)
+
+const (
+	accountName      = "devstoreaccount1"
+	accountKey       = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+	endpoint         = "127.0.0.1:10010"
+	protocol         = "http"
+	containerName    = "warehouse"
+	connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10010/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
path: io/azure_integration_test.go
user: login: xuhui-lu
body: turn to use string template here.
created_at: 2025-04-01T18:29:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 10010:10000
path: dev/docker-compose.yml
user: login: xuhui-lu
body: Turn to use port 11000 here.
created_at: 2025-04-01T18:30:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 10010:10000
path: dev/docker-compose.yml
user: login: kevinjqliu
body: hm good catch. i dont think port `10000` and `10001` are used by the spark container
https://github.com/apache/iceberg-go/blob/19a651409cceafc6e04375f80466b3aa124bc78b/dev/docker-compose.yml#L37-L38  

this setup is [copied over from pyiceberg](https://github.com/apache/iceberg-python/blob/4d4714a46241d0d89519a2a605dbce27b713a60e/dev/docker-compose-integration.yml#L35-L37) which doesnt use those ports

i think we can remove those 2 ports from the spark container and use it in the azure container. i would prefer this


alternatively we can use `11000` but map it to the same port (`11000:11000`). Its good practice to reuse the same port when redirecting 
created_at: 2025-04-01T19:37:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 10010:10000
path: dev/docker-compose.yml
user: login: xuhui-lu
body: thanks for the clarification. Let me update it.
created_at: 2025-04-02T02:28:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 10010:10000
path: dev/docker-compose.yml
user: login: xuhui-lu
body: I use the alternative here since it will break the service when I try to port it to 10000. I guess it might have a conflict with existing resources.
created_at: 2025-04-02T04:15:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,13 @@ services:
       - 9001:9001
       - 9000:9000
     command: ["server", "/data", "--console-address", ":9001"]
+  azurite:
+    image: mcr.microsoft.com/azure-storage/azurite
+    container_name: azurite
+    hostname: azurite
+    ports:
+      - 10010:10000
path: dev/docker-compose.yml
user: login: xuhui-lu
body: fixed.
created_at: 2025-04-02T04:15:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: xuhui-lu
created_at: 2025-03-10T05:02:38Z
body: > @kevinjqliu @Fokko do you know of any equivalent to running Minio that we could use via a docker image to test the ADLS integration?

I am not sure if I could just use the https://github.com/Azure/Azurite here. Will take a try locally.
author_association: CONTRIBUTOR
user: login: kevinjqliu
created_at: 2025-03-11T16:37:54Z
body: we spin up an azurite container for azure related integration tests 
https://github.com/apache/iceberg-python/blob/764880364c94fbc4d29a0677350463de1d94e75c/dev/docker-compose-azurite.yml
author_association: CONTRIBUTOR
user: login: dwilson1988
created_at: 2025-03-12T14:00:40Z
body: Hey, sorry, I had promised to help out on adding in azure support, but got busy. Happy to help if you need additional assistance.
author_association: CONTRIBUTOR
user: login: piyushsingariya
created_at: 2025-03-29T08:43:26Z
body: What's the status on this? @xuhui-lu 
author_association: NONE
user: login: xuhui-lu
created_at: 2025-03-29T15:15:09Z
body: > What's the status on this? @xuhui-lu

I will try to mock the azure blob store integration locally this weekend like what pyiceberg did. 
author_association: CONTRIBUTOR
user: login: xuhui-lu
created_at: 2025-03-29T18:52:20Z
body: > What's the status on this? @xuhui-lu

make some progress locally, will submit it once the local test is passed.
author_association: CONTRIBUTOR
user: login: xuhui-lu
created_at: 2025-03-30T18:45:33Z
body: hey @piyushsingariya , just complete the local test and it should be ready to be reviewed. 
author_association: CONTRIBUTOR
user: login: zeroshade
created_at: 2025-03-31T21:18:54Z
body: @xuhui-lu looks like the integration test still fails. otherwise things look pretty good to me. just need to get the tests fixed
author_association: MEMBER
user: login: xuhui-lu
created_at: 2025-04-01T01:50:55Z
body: > @xuhui-lu looks like the integration test still fails. otherwise things look pretty good to me. just need to get the tests fixed

Not sure what happened on ci. Let me just verify it again locally.
author_association: CONTRIBUTOR
user: login: xuhui-lu
created_at: 2025-04-01T03:23:32Z
body: > @xuhui-lu looks like the integration test still fails. otherwise things look pretty good to me. just need to get the tests fixed

it is a docker-compose issue, I've fixed it.
author_association: CONTRIBUTOR
user: login: xuhui-lu
created_at: 2025-04-01T17:03:12Z
body: removed extra environment variable. The test could pass now.
author_association: CONTRIBUTOR