Repository: apache/iceberg-go
Pull request #260 (2025-01-19 22:04:54)

feat(catalog): Have Load use "type" property and "name" for config
As brought up in https://github.com/apache/iceberg-go/pull/244#discussion_r1911257805 this PR implements using a "type" property when loading catalogs and looking up catalog configurations using the provided "name", only using the uri scheme as a fallback when necessary.


Patch:
diff --git a/catalog/registry.go b/catalog/registry.go
index 1c88351..544d419 100644
--- a/catalog/registry.go
+++ b/catalog/registry.go
@@ -26,6 +26,7 @@ import (
 	"sync"
 
 	"github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/config"
 )
 
 type registry map[string]Registrar
@@ -97,13 +98,23 @@ func GetRegisteredCatalogs() []string {
 // easier catalog loading but also to allow for custom catalog implementations to
 // be registered and loaded external to this module.
 //
-// The URI is used to determine the catalog type by first checking if it contains
-// the string "://" indicating the presence of a scheme. If so, the scheme is used
-// to lookup the registered catalog. i.e. "glue://..." would return the Glue catalog
-// implementation, passing the URI and properties to NewGlueCatalog. If no scheme is
-// present, then the URI is used as-is to lookup the catalog factory function.
+// The name parameter is used to lookup the catalog configuration, if one exists,
+// that was loaded from the configuration file, ".iceberg-go.yaml". By default,
+// the config file is loaded from the user's home directory, but the directory can
+// be changed by setting the GOICEBERG_HOME environment variable to the path of the
+// directory containing the ".iceberg-go.yaml" file. The name will also be passed to
+// the registered GetCatalog function.
 //
-// Currently the following catalogs are registered by default:
+// The catalog registry will be checked for the catalog's type. The "type" property
+// is used first to search the catalog registry, with the passed properties taking
+// priority over any loaded config.
+//
+// If there is no "type" in the configuration and no "type" in the passed in properties,
+// then the "uri" property is used to lookup the catalog by checking the scheme. Again,
+// if there is a "uri" key set in the passed in "props" it will take priority over the
+// configuration.
+//
+// Currently the following catalog types are registered by default:
 //
 //   - "glue" for AWS Glue Data Catalog, the rest of the URI is ignored, all configuration
 //     should be provided using the properties. "glue.region", "glue.endpoint",
@@ -114,16 +125,33 @@ func GetRegisteredCatalogs() []string {
 //     as the REST endpoint, otherwise the URI is used as the endpoint. The REST catalog also
 //     registers "http" and "https" so that Load with an http/s URI will automatically
 //     load the REST Catalog.
-func Load(catalogURI string, props iceberg.Properties) (Catalog, error) {
-	var catalogType string
-	if strings.Contains(catalogURI, "://") {
-		parsed, err := url.Parse(catalogURI)
-		if err != nil {
-			return nil, err
+func Load(name string, props iceberg.Properties) (Catalog, error) {
+	if name == "" {
+		name = config.EnvConfig.DefaultCatalog
+	}
+
+	conf := config.EnvConfig.Catalogs[name]
+	if props == nil {
+		props = iceberg.Properties{
+			"uri":        conf.URI,
+			"credential": conf.Credential,
+			"warehouse":  conf.Warehouse,
 		}
-		catalogType = parsed.Scheme
 	} else {
-		catalogType = catalogURI
+		props["uri"] = props.Get("uri", conf.URI)
+		props["credential"] = props.Get("credential", conf.Credential)
+		props["warehouse"] = props.Get("warehouse", conf.Warehouse)
+	}
+
+	catalogType := props.Get("type", conf.CatalogType)
+	if catalogType == "" {
+		if strings.Contains(props["uri"], "://") {
+			uri, err := url.Parse(props["uri"])
+			if err != nil {
+				return nil, fmt.Errorf("failed to parse catalog URI: %w", err)
+			}
+			catalogType = uri.Scheme
+		}
 	}
 
 	cat, ok := defaultRegistry.get(catalogType)
@@ -131,5 +159,5 @@ func Load(catalogURI string, props iceberg.Properties) (Catalog, error) {
 		return nil, fmt.Errorf("%w: %s", ErrCatalogNotFound, catalogType)
 	}
 
-	return cat.GetCatalog(catalogURI, props)
+	return cat.GetCatalog(name, props)
 }
diff --git a/catalog/rest.go b/catalog/rest.go
index efb9217..102d82a 100644
--- a/catalog/rest.go
+++ b/catalog/rest.go
@@ -74,8 +74,8 @@ var (
 )
 
 func init() {
-	reg := RegistrarFunc(func(endpoint string, p iceberg.Properties) (Catalog, error) {
-		return newRestCatalogFromProps(endpoint, p.Get("uri", endpoint), p)
+	reg := RegistrarFunc(func(name string, p iceberg.Properties) (Catalog, error) {
+		return newRestCatalogFromProps(name, p.Get("uri", ""), p)
 	})
 
 	Register(string(REST), reg)
@@ -595,6 +595,9 @@ func (r *RestCatalog) fetchConfig(opts *options) (*options, error) {
 	}
 
 	cfg := rsp.Defaults
+	if cfg == nil {
+		cfg = iceberg.Properties{}
+	}
 	maps.Copy(cfg, toProps(opts))
 	maps.Copy(cfg, rsp.Overrides)
 
@@ -613,6 +616,7 @@ func (r *RestCatalog) fetchConfig(opts *options) (*options, error) {
 	return o, nil
 }
 
+func (r *RestCatalog) Name() string             { return r.name }
 func (r *RestCatalog) CatalogType() CatalogType { return REST }
 
 func checkValidNamespace(ident table.Identifier) error {
diff --git a/cmd/iceberg/main.go b/cmd/iceberg/main.go
index 8db7cc7..00ceec9 100644
--- a/cmd/iceberg/main.go
+++ b/cmd/iceberg/main.go
@@ -395,7 +395,7 @@ func properties(output Output, cat catalog.Catalog, args propCmd) {
 
 func mergeConf(fileConf *config.CatalogConfig, resConfig *Config) {
 	if len(resConfig.Catalog) == 0 {
-		resConfig.Catalog = fileConf.Catalog
+		resConfig.Catalog = fileConf.CatalogType
 	}
 	if len(resConfig.URI) == 0 {
 		resConfig.URI = fileConf.URI
diff --git a/config/config.go b/config/config.go
index 4af7654..ef10d2f 100644
--- a/config/config.go
+++ b/config/config.go
@@ -24,18 +24,21 @@ import (
 	"gopkg.in/yaml.v3"
 )
 
-const cfgFile = ".iceberg-go.yaml"
+const (
+	cfgFile = ".iceberg-go.yaml"
+)
 
 type Config struct {
-	Catalogs map[string]CatalogConfig `yaml:"catalog"`
+	DefaultCatalog string                   `yaml:"default-catalog"`
+	Catalogs       map[string]CatalogConfig `yaml:"catalog"`
 }
 
 type CatalogConfig struct {
-	Catalog    string `yaml:"catalog"`
-	URI        string `yaml:"uri"`
-	Output     string `yaml:"output"`
-	Credential string `yaml:"credential"`
-	Warehouse  string `yaml:"warehouse"`
+	CatalogType string `yaml:"type"`
+	URI         string `yaml:"uri"`
+	Output      string `yaml:"output"`
+	Credential  string `yaml:"credential"`
+	Warehouse   string `yaml:"warehouse"`
 }
 
 func LoadConfig(configPath string) []byte {
@@ -69,3 +72,23 @@ func ParseConfig(file []byte, catalogName string) *CatalogConfig {
 	}
 	return &res
 }
+
+func fromConfigFiles() Config {
+	dir := os.Getenv("GOICEBERG_HOME")
+	if dir != "" {
+		dir = filepath.Join(dir, cfgFile)
+	}
+
+	var cfg Config
+	if err := yaml.Unmarshal(LoadConfig(dir), &cfg); err != nil {
+		return cfg
+	}
+
+	if cfg.DefaultCatalog == "" {
+		cfg.DefaultCatalog = "default"
+	}
+
+	return cfg
+}
+
+var EnvConfig = fromConfigFiles()
diff --git a/go.mod b/go.mod
index e18c4ee..944e030 100644
--- a/go.mod
+++ b/go.mod
@@ -23,11 +23,11 @@ toolchain go1.23.2
 
 require (
 	github.com/apache/arrow-go/v18 v18.1.0
-	github.com/aws/aws-sdk-go-v2 v1.32.8
-	github.com/aws/aws-sdk-go-v2/config v1.28.11
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.52
-	github.com/aws/aws-sdk-go-v2/service/glue v1.105.1
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2
+	github.com/aws/aws-sdk-go-v2 v1.33.0
+	github.com/aws/aws-sdk-go-v2/config v1.29.1
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.54
+	github.com/aws/aws-sdk-go-v2/service/glue v1.105.3
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2
 	github.com/aws/smithy-go v1.22.1
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
 	github.com/google/uuid v1.6.0
@@ -38,7 +38,7 @@ require (
 	github.com/twmb/murmur3 v1.1.8
 	gocloud.dev v0.40.0
 	golang.org/x/sync v0.10.0
-	google.golang.org/api v0.216.0
+	google.golang.org/api v0.217.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -47,8 +47,8 @@ require (
 	atomicgo.dev/keyboard v0.2.9 // indirect
 	atomicgo.dev/schedule v0.1.0 // indirect
 	cloud.google.com/go v0.116.0 // indirect
-	cloud.google.com/go/auth v0.13.0 // indirect
-	cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
+	cloud.google.com/go/auth v0.14.0 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
 	cloud.google.com/go/compute/metadata v0.6.0 // indirect
 	cloud.google.com/go/iam v1.2.2 // indirect
 	cloud.google.com/go/storage v1.43.0 // indirect
@@ -58,19 +58,19 @@ require (
 	github.com/apache/thrift v0.21.0 // indirect
 	github.com/aws/aws-sdk-go v1.55.5 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.11 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect
 	github.com/containerd/console v1.0.3 // indirect
 	github.com/creasty/defaults v1.8.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -83,7 +83,7 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/flatbuffers v24.12.23+incompatible // indirect
-	github.com/google/s2a-go v0.1.8 // indirect
+	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/google/wire v0.6.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
 	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
@@ -128,7 +128,7 @@ require (
 	golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
 	google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
-	google.golang.org/grpc v1.69.2 // indirect
-	google.golang.org/protobuf v1.36.1 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
+	google.golang.org/grpc v1.69.4 // indirect
+	google.golang.org/protobuf v1.36.2 // indirect
 )
diff --git a/go.sum b/go.sum
index 2ae76bb..3d8225e 100644
--- a/go.sum
+++ b/go.sum
@@ -9,10 +9,10 @@ atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8p
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
 cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
-cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
-cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
-cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
-cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
+cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
+cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
+cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
+cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
 cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
 cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
@@ -44,50 +44,44 @@ github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07
 github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
 github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
 github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
-github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=
-github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
+github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs=
+github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
-github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg=
-github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg=
-github.com/aws/aws-sdk-go-v2/config v1.28.11 h1:7Ekru0IkRHRnSRWGQLnLN6i0o1Jncd0rHo2T130+tEQ=
-github.com/aws/aws-sdk-go-v2/config v1.28.11/go.mod h1:x78TpPvBfHH16hi5tE3OCWQ0pzNfyXA349p5/Wp82Yo=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.52 h1:I4ymSk35LHogx2Re2Wu6LOHNTRaRWkLVoJgWS5Wd40M=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.52/go.mod h1:vAkqKbMNUcher8fDXP2Ge2qFXKMkcD74qvk1lJRMemM=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
+github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=
+github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.11 h1:FEDZD/Axt5tKSkPAs967KZ++MkvYdBqr0a+cetRbjLM=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.11/go.mod h1:dvlsbA32KfvCzqwTiX7maABgFek2RyUuYEJ3kyn/PmQ=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27 h1:AmB5QxnD+fBFrg9LcqzkgF/CaYvMyU/BTlejG4t1S7Q=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27/go.mod h1:Sai7P3xTiyv9ZUYO3IFxMnmiIP759/67iQbU4kdmkyU=
-github.com/aws/aws-sdk-go-v2/service/glue v1.105.1 h1:mDsGQACN5uam2cqa8JBqDQPD4ANk1rnoCIwvXl/lBxo=
-github.com/aws/aws-sdk-go-v2/service/glue v1.105.1/go.mod h1:HAzAQ4+VxKxrAB6cQ6/5MpqnMt9G6+zW4NVT/njQzKw=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 h1:7kpeALOUeThs2kEjlAxlADAVfxKmkYAedlpZ3kdoSJ4=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28/go.mod h1:pyaOYEdp1MJWgtXLy6q80r3DhsVdOIOZNB9hdTcJIvI=
+github.com/aws/aws-sdk-go-v2/service/glue v1.105.3 h1:jON0ezP0yT9Z9LOsIhDUsR35Vsr4mVblfr09c8hTefI=
+github.com/aws/aws-sdk-go-v2/service/glue v1.105.3/go.mod h1:Q4HCfM94gpSI7sMLKooKo8PyQYhLFFHABhKmQPcdVbI=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8 h1:iwYS40JnrBeA9e9aI5S6KKN4EB2zR4iUVYN0nwVivz4=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8/go.mod h1:Fm9Mi+ApqmFiknZtGpohVcBGvpTu542VC4XO9YudRi0=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 h1:/Mn7gTedG86nbpjT4QEKsN1D/fThiYe1qvq7WsBGNHg=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8/go.mod h1:Ae3va9LPmvjj231ukHB6UeT8nS7wTPfC3tMZSZMwNYg=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2 h1:a7aQ3RW+ug4IbhoQp29NZdc7vqrzKZZfWZSaQAXOZvQ=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2/go.mod h1:xMekrnhmJ5aqmyxtmALs7mlvXw5xRh+eYjOjvrIIFJ4=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.7 h1:qwGa9MA8G7mBq2YphHFaygdPe5t9OA7SvaJdwWTlEds=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.7/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 h1:e6um6+DWYQP1XCa+E9YVtG/9v1qk5lyAOelMOVwSyO8=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2/go.mod h1:dIW8puxSbYLSPv/ju0d9A3CpwXdtqvJtYKDMVmPLOWE=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 h1:2aInXbh02XsbO0KobPGMNXyv2QP73VDKsWPNJARj/+4=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9/go.mod h1:dgXS1i+HgWnYkPXqNoPIPKeUsUUYHaUbThC90aDnNiE=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 h1:F3h8VYq9ZLBXYurmwrT8W0SPhgCcU0q+0WZJfT1dFt0=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2/go.mod h1:jGJ/v7FIi7Ys9t54tmEFnrxuaWeJLpwNgKp2DXAVhOU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=
 github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
 github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -162,8 +156,8 @@ github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
 github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
-github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
-github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -399,8 +393,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
 gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
 gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
-google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY=
-google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI=
+google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU=
+google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -410,15 +404,15 @@ google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f h1:zDoHYmMzMacIdjN
 google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f/go.mod h1:Q5m6g8b5KaFFzsQFIGdJkSJDGeJiybVenoYFMMa3ohI=
 google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
 google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
-google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
+google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
+google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -428,8 +422,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
-google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
+google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=


Test patch:
diff --git a/catalog/registry_test.go b/catalog/registry_test.go
index c132656..77347a7 100644
--- a/catalog/registry_test.go
+++ b/catalog/registry_test.go
@@ -18,10 +18,15 @@
 package catalog_test
 
 import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
 	"testing"
 
 	"github.com/apache/iceberg-go"
 	"github.com/apache/iceberg-go/catalog"
+	"github.com/apache/iceberg-go/config"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -49,15 +54,28 @@ func TestCatalogRegistry(t *testing.T) {
 
 	c, err := catalog.Load("foobar", iceberg.Properties{"foo": "baz"})
 	assert.Nil(t, c)
+	assert.ErrorIs(t, err, catalog.ErrCatalogNotFound)
+
+	catalog.Register("foobar", catalog.RegistrarFunc(func(s string, p iceberg.Properties) (catalog.Catalog, error) {
+		assert.Equal(t, "not found", s)
+		assert.Equal(t, "baz", p.Get("foo", ""))
+		return nil, nil
+	}))
+
+	c, err = catalog.Load("not found", iceberg.Properties{"type": "foobar", "foo": "baz"})
+	assert.Nil(t, c)
 	assert.NoError(t, err)
 
 	catalog.Register("foobar", catalog.RegistrarFunc(func(s string, p iceberg.Properties) (catalog.Catalog, error) {
-		assert.Equal(t, "foobar://helloworld", s)
+		assert.Equal(t, "not found", s)
+		assert.Equal(t, "foobar://helloworld", p.Get("uri", ""))
 		assert.Equal(t, "baz", p.Get("foo", ""))
 		return nil, nil
 	}))
 
-	c, err = catalog.Load("foobar://helloworld", iceberg.Properties{"foo": "baz"})
+	c, err = catalog.Load("not found", iceberg.Properties{
+		"uri": "foobar://helloworld",
+		"foo": "baz"})
 	assert.Nil(t, c)
 	assert.NoError(t, err)
 
@@ -69,3 +87,54 @@ func TestCatalogRegistry(t *testing.T) {
 		"glue",
 	}, catalog.GetRegisteredCatalogs())
 }
+
+func TestRegistryFromConfig(t *testing.T) {
+	var params url.Values
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/v1/config", func(w http.ResponseWriter, r *http.Request) {
+		params = r.URL.Query()
+
+		json.NewEncoder(w).Encode(map[string]any{
+			"default":   map[string]any{},
+			"overrides": map[string]any{},
+		})
+	})
+
+	srv := httptest.NewServer(mux)
+
+	defer func(cats map[string]config.CatalogConfig) {
+		config.EnvConfig.Catalogs = cats
+	}(config.EnvConfig.Catalogs)
+
+	config.EnvConfig.Catalogs = map[string]config.CatalogConfig{
+		"foobar": {
+			CatalogType: "rest",
+			URI:         srv.URL,
+			Warehouse:   "catalog_name",
+		},
+	}
+
+	c, err := catalog.Load("foobar", nil)
+	assert.NoError(t, err)
+	assert.IsType(t, &catalog.RestCatalog{}, c)
+	assert.Equal(t, "foobar", c.(*catalog.RestCatalog).Name())
+	assert.Equal(t, "catalog_name", params.Get("warehouse"))
+
+	c, err = catalog.Load("foobar", iceberg.Properties{"warehouse": "overriden"})
+	assert.NoError(t, err)
+	assert.IsType(t, &catalog.RestCatalog{}, c)
+	assert.Equal(t, "foobar", c.(*catalog.RestCatalog).Name())
+	assert.Equal(t, "overriden", params.Get("warehouse"))
+
+	srv.Close()
+
+	srv2 := httptest.NewServer(mux)
+	defer srv2.Close()
+
+	c, err = catalog.Load("foobar", iceberg.Properties{"uri": srv2.URL})
+	assert.NoError(t, err)
+	assert.IsType(t, &catalog.RestCatalog{}, c)
+	assert.Equal(t, "foobar", c.(*catalog.RestCatalog).Name())
+	assert.Equal(t, "catalog_name", params.Get("warehouse"))
+}
diff --git a/catalog/rest_test.go b/catalog/rest_test.go
index 81ebbaf..479fd70 100644
--- a/catalog/rest_test.go
+++ b/catalog/rest_test.go
@@ -137,7 +137,8 @@ func (r *RestCatalogSuite) TestLoadRegisteredCatalog() {
 		})
 	})
 
-	cat, err := catalog.Load(r.srv.URL, iceberg.Properties{
+	cat, err := catalog.Load("restful", iceberg.Properties{
+		"uri":        r.srv.URL,
 		"warehouse":  "s3://some-bucket",
 		"credential": TestCreds,
 	})
@@ -758,42 +759,41 @@ func (r *RestCatalogSuite) TestCreateTable200() {
 }
 
 func (r *RestCatalogSuite) TestCreateTable409() {
-    // Mock the create table endpoint with 409 response
-    r.mux.HandleFunc("/v1/namespaces/fokko/tables", func(w http.ResponseWriter, req *http.Request) {
-        r.Require().Equal(http.MethodPost, req.Method)
-
-        for k, v := range TestHeaders {
-            r.Equal(v, req.Header.Values(k))
-        }
-
-        w.WriteHeader(http.StatusConflict)
-        errorResponse := map[string]interface{}{
-            "error": map[string]interface{}{
-                "message": "Table already exists: fokko.already_exists in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
-                "type":    "AlreadyExistsException",
-                "code":    409,
-            },
-        }
-        json.NewEncoder(w).Encode(errorResponse)
-    })
-
-    cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
-    r.Require().NoError(err)
-
-    // Attempt to create table with properties
-    _, err = cat.CreateTable(
-        context.Background(),
-        catalog.ToRestIdentifier("fokko", "fokko2"),
-        tableSchemaSimple,
-        catalog.WithProperties(map[string]string{"owner": "fokko"}),
-    )
-
-    // Verify error
-    r.Error(err)
-    r.Contains(err.Error(), "Table already exists")
-    r.ErrorIs(err, catalog.ErrTableAlreadyExists)
-}
+	// Mock the create table endpoint with 409 response
+	r.mux.HandleFunc("/v1/namespaces/fokko/tables", func(w http.ResponseWriter, req *http.Request) {
+		r.Require().Equal(http.MethodPost, req.Method)
+
+		for k, v := range TestHeaders {
+			r.Equal(v, req.Header.Values(k))
+		}
+
+		w.WriteHeader(http.StatusConflict)
+		errorResponse := map[string]interface{}{
+			"error": map[string]interface{}{
+				"message": "Table already exists: fokko.already_exists in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
+				"type":    "AlreadyExistsException",
+				"code":    409,
+			},
+		}
+		json.NewEncoder(w).Encode(errorResponse)
+	})
+
+	cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
+	r.Require().NoError(err)
+
+	// Attempt to create table with properties
+	_, err = cat.CreateTable(
+		context.Background(),
+		catalog.ToRestIdentifier("fokko", "fokko2"),
+		tableSchemaSimple,
+		catalog.WithProperties(map[string]string{"owner": "fokko"}),
+	)
 
+	// Verify error
+	r.Error(err)
+	r.Contains(err.Error(), "Table already exists")
+	r.ErrorIs(err, catalog.ErrTableAlreadyExists)
+}
 
 func (r *RestCatalogSuite) TestLoadTable200() {
 	r.mux.HandleFunc("/v1/namespaces/fokko/tables/table", func(w http.ResponseWriter, req *http.Request) {
@@ -915,113 +915,112 @@ func (r *RestCatalogSuite) TestLoadTable200() {
 }
 
 func (r *RestCatalogSuite) TestRenameTable200() {
-    // Mock the rename table endpoint
-    r.mux.HandleFunc("/v1/tables/rename", func(w http.ResponseWriter, req *http.Request) {
-        r.Require().Equal(http.MethodPost, req.Method)
-
-        for k, v := range TestHeaders {
-            r.Equal(v, req.Header.Values(k))
-        }
-
-        var payload struct {
-            From struct {
-                Namespace []string `json:"namespace"`
-                Name     string   `json:"name"`
-            } `json:"from"`
-            To struct {
-                Namespace []string `json:"namespace"`
-                Name     string   `json:"name"`
-            } `json:"to"`
-        }
-        r.NoError(json.NewDecoder(req.Body).Decode(&payload))
-        r.Equal([]string{"fokko"}, payload.From.Namespace)
-        r.Equal("source", payload.From.Name)
-        r.Equal([]string{"fokko"}, payload.To.Namespace)
-        r.Equal("destination", payload.To.Name)
-
-        w.WriteHeader(http.StatusOK)
-    })
-
-    // Mock the get table endpoint for loading the renamed table
-    r.mux.HandleFunc("/v1/namespaces/fokko/tables/destination", func(w http.ResponseWriter, req *http.Request) {
-        r.Require().Equal(http.MethodGet, req.Method)
-
-        for k, v := range TestHeaders {
-            r.Equal(v, req.Header.Values(k))
-        }
-
-        w.Write([]byte(createTableRestExample))
-    })
-
-    cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
-    r.Require().NoError(err)
-
-    fromIdent := catalog.ToRestIdentifier("fokko", "source")
-    toIdent := catalog.ToRestIdentifier("fokko", "destination")
-
-    renamedTable, err := cat.RenameTable(context.Background(), fromIdent, toIdent)
-    r.Require().NoError(err)
-
-    r.Equal(catalog.ToRestIdentifier("rest", "fokko", "destination"), renamedTable.Identifier())
-    r.Equal("s3://warehouse/database/table/metadata.json", renamedTable.MetadataLocation())
-    r.EqualValues(1, renamedTable.Metadata().Version())
-    r.Equal("bf289591-dcc0-4234-ad4f-5c3eed811a29", renamedTable.Metadata().TableUUID().String())
-    r.EqualValues(1657810967051, renamedTable.Metadata().LastUpdatedMillis())
-    r.Equal(3, renamedTable.Metadata().LastColumnID())
-    r.Zero(renamedTable.Schema().ID)
-    r.Zero(renamedTable.Metadata().DefaultPartitionSpec())
-    r.Equal(999, *renamedTable.Metadata().LastPartitionSpecID())
-    r.Equal(table.UnsortedSortOrder, renamedTable.SortOrder())
-}
+	// Mock the rename table endpoint
+	r.mux.HandleFunc("/v1/tables/rename", func(w http.ResponseWriter, req *http.Request) {
+		r.Require().Equal(http.MethodPost, req.Method)
+
+		for k, v := range TestHeaders {
+			r.Equal(v, req.Header.Values(k))
+		}
+
+		var payload struct {
+			From struct {
+				Namespace []string `json:"namespace"`
+				Name      string   `json:"name"`
+			} `json:"from"`
+			To struct {
+				Namespace []string `json:"namespace"`
+				Name      string   `json:"name"`
+			} `json:"to"`
+		}
+		r.NoError(json.NewDecoder(req.Body).Decode(&payload))
+		r.Equal([]string{"fokko"}, payload.From.Namespace)
+		r.Equal("source", payload.From.Name)
+		r.Equal([]string{"fokko"}, payload.To.Namespace)
+		r.Equal("destination", payload.To.Name)
+
+		w.WriteHeader(http.StatusOK)
+	})
+
+	// Mock the get table endpoint for loading the renamed table
+	r.mux.HandleFunc("/v1/namespaces/fokko/tables/destination", func(w http.ResponseWriter, req *http.Request) {
+		r.Require().Equal(http.MethodGet, req.Method)
+
+		for k, v := range TestHeaders {
+			r.Equal(v, req.Header.Values(k))
+		}
+
+		w.Write([]byte(createTableRestExample))
+	})
 
+	cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
+	r.Require().NoError(err)
+
+	fromIdent := catalog.ToRestIdentifier("fokko", "source")
+	toIdent := catalog.ToRestIdentifier("fokko", "destination")
+
+	renamedTable, err := cat.RenameTable(context.Background(), fromIdent, toIdent)
+	r.Require().NoError(err)
+
+	r.Equal(catalog.ToRestIdentifier("rest", "fokko", "destination"), renamedTable.Identifier())
+	r.Equal("s3://warehouse/database/table/metadata.json", renamedTable.MetadataLocation())
+	r.EqualValues(1, renamedTable.Metadata().Version())
+	r.Equal("bf289591-dcc0-4234-ad4f-5c3eed811a29", renamedTable.Metadata().TableUUID().String())
+	r.EqualValues(1657810967051, renamedTable.Metadata().LastUpdatedMillis())
+	r.Equal(3, renamedTable.Metadata().LastColumnID())
+	r.Zero(renamedTable.Schema().ID)
+	r.Zero(renamedTable.Metadata().DefaultPartitionSpec())
+	r.Equal(999, *renamedTable.Metadata().LastPartitionSpecID())
+	r.Equal(table.UnsortedSortOrder, renamedTable.SortOrder())
+}
 
 func (r *RestCatalogSuite) TestDropTable204() {
-    // Mock the drop table endpoint
-    r.mux.HandleFunc("/v1/namespaces/fokko/tables/table", func(w http.ResponseWriter, req *http.Request) {
-        r.Require().Equal(http.MethodDelete, req.Method)
+	// Mock the drop table endpoint
+	r.mux.HandleFunc("/v1/namespaces/fokko/tables/table", func(w http.ResponseWriter, req *http.Request) {
+		r.Require().Equal(http.MethodDelete, req.Method)
 
-        for k, v := range TestHeaders {
-            r.Equal(v, req.Header.Values(k))
-        }
+		for k, v := range TestHeaders {
+			r.Equal(v, req.Header.Values(k))
+		}
 
-        // Return 204 No Content for successful deletion
-        w.WriteHeader(http.StatusNoContent)
-    })
+		// Return 204 No Content for successful deletion
+		w.WriteHeader(http.StatusNoContent)
+	})
 
-    cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
-    r.Require().NoError(err)
+	cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
+	r.Require().NoError(err)
 
-    err = cat.DropTable(context.Background(), catalog.ToRestIdentifier("fokko", "table"))
-    r.NoError(err)
+	err = cat.DropTable(context.Background(), catalog.ToRestIdentifier("fokko", "table"))
+	r.NoError(err)
 }
 
 func (r *RestCatalogSuite) TestDropTable404() {
-    // Mock the drop table endpoint with 404 response
-    r.mux.HandleFunc("/v1/namespaces/fokko/tables/table", func(w http.ResponseWriter, req *http.Request) {
-        r.Require().Equal(http.MethodDelete, req.Method)
-
-        for k, v := range TestHeaders {
-            r.Equal(v, req.Header.Values(k))
-        }
-
-        w.WriteHeader(http.StatusNotFound)
-        errorResponse := map[string]interface{}{
-            "error": map[string]interface{}{
-                "message": "Table does not exist: fokko.table",
-                "type":    "NoSuchTableException",
-                "code":    404,
-            },
-        }
-        json.NewEncoder(w).Encode(errorResponse)
-    })
-
-    cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
-    r.Require().NoError(err)
-
-    err = cat.DropTable(context.Background(), catalog.ToRestIdentifier("fokko", "table"))
-    r.Error(err)
-    r.ErrorIs(err, catalog.ErrNoSuchTable)
-    r.ErrorContains(err, "Table does not exist: fokko.table")
+	// Mock the drop table endpoint with 404 response
+	r.mux.HandleFunc("/v1/namespaces/fokko/tables/table", func(w http.ResponseWriter, req *http.Request) {
+		r.Require().Equal(http.MethodDelete, req.Method)
+
+		for k, v := range TestHeaders {
+			r.Equal(v, req.Header.Values(k))
+		}
+
+		w.WriteHeader(http.StatusNotFound)
+		errorResponse := map[string]interface{}{
+			"error": map[string]interface{}{
+				"message": "Table does not exist: fokko.table",
+				"type":    "NoSuchTableException",
+				"code":    404,
+			},
+		}
+		json.NewEncoder(w).Encode(errorResponse)
+	})
+
+	cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken))
+	r.Require().NoError(err)
+
+	err = cat.DropTable(context.Background(), catalog.ToRestIdentifier("fokko", "table"))
+	r.Error(err)
+	r.ErrorIs(err, catalog.ErrNoSuchTable)
+	r.ErrorContains(err, "Table does not exist: fokko.table")
 }
 
 type RestTLSCatalogSuite struct {
@@ -1064,7 +1063,8 @@ func (r *RestTLSCatalogSuite) TestSSLFail() {
 }
 
 func (r *RestTLSCatalogSuite) TestSSLLoadRegisteredCatalog() {
-	cat, err := catalog.Load(r.srv.URL, iceberg.Properties{
+	cat, err := catalog.Load("foobar", iceberg.Properties{
+		"uri":                  r.srv.URL,
 		"warehouse":            "s3://some-bucket",
 		"token":                TestToken,
 		"rest.tls.skip-verify": "true",
diff --git a/config/config_test.go b/config/config_test.go
index 3e177d5..4abae4b 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -34,7 +34,7 @@ var testArgs = []struct {
 	{[]byte(`
 catalog:
   custom-catalog:
-    catalog: rest
+    type: rest
     uri: http://localhost:8181/
     output: text
     credential: client-id:client-secret
@@ -44,35 +44,35 @@ catalog:
 	{[]byte(`
 catalog:
   default:
-    catalog: rest
+    type: rest
     uri: http://localhost:8181/
     output: text
     credential: client-id:client-secret
     warehouse: catalog_name
 `), "default",
 		&CatalogConfig{
-			Catalog:    "rest",
-			URI:        "http://localhost:8181/",
-			Output:     "text",
-			Credential: "client-id:client-secret",
-			Warehouse:  "catalog_name",
+			CatalogType: "rest",
+			URI:         "http://localhost:8181/",
+			Output:      "text",
+			Credential:  "client-id:client-secret",
+			Warehouse:   "catalog_name",
 		}},
 	// custom catalog
 	{[]byte(`
 catalog:
   custom-catalog:
-    catalog: rest
+    type: rest
     uri: http://localhost:8181/
     output: text
     credential: client-id:client-secret
     warehouse: catalog_name
 `), "custom-catalog",
 		&CatalogConfig{
-			Catalog:    "rest",
-			URI:        "http://localhost:8181/",
-			Output:     "text",
-			Credential: "client-id:client-secret",
-			Warehouse:  "catalog_name",
+			CatalogType: "rest",
+			URI:         "http://localhost:8181/",
+			Output:      "text",
+			Credential:  "client-id:client-secret",
+			Warehouse:   "catalog_name",
 		}},
 }