nastra commented on code in PR #58: URL: https://github.com/apache/iceberg-go/pull/58#discussion_r1486261377
########## catalog/rest_test.go: ########## @@ -0,0 +1,818 @@ +// 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 catalog_test + +import ( + "context" + "crypto/tls" + "crypto/x509" + "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/table" + "github.com/stretchr/testify/suite" +) + +const ( + TestCreds = "client:secret" + TestToken = "some_jwt_token" +) + +var ( + TestHeaders = http.Header{ + "X-Client-Version": {"0.14.1"}, + "User-Agent": {"GoIceberg/" + iceberg.Version()}, + "Authorization": {"Bearer " + TestToken}, + } + OAuthTestHeaders = http.Header{ + "Content-Type": {"application/x-www-form-urlencoded"}, + } +) + +type RestCatalogSuite struct { + suite.Suite + + srv *httptest.Server + mux *http.ServeMux + + configVals url.Values +} + +func (r *RestCatalogSuite) SetupTest() { + r.mux = http.NewServeMux() + + r.mux.HandleFunc("/v1/config", func(w http.ResponseWriter, req *http.Request) { + r.Require().Equal(http.MethodGet, req.Method) + r.configVals = req.URL.Query() + + json.NewEncoder(w).Encode(map[string]any{ + "defaults": map[string]any{}, + "overrides": map[string]any{}, + }) + }) + + r.srv = httptest.NewServer(r.mux) +} + +func (r *RestCatalogSuite) TearDownTest() { + r.srv.Close() + r.srv = nil + r.mux = nil + r.configVals = nil +} + +func (r *RestCatalogSuite) TestToken200() { + r.mux.HandleFunc("/v1/oauth/tokens", func(w http.ResponseWriter, req *http.Request) { + r.Equal(http.MethodPost, req.Method) + + r.Equal(req.Header.Get("Content-Type"), "application/x-www-form-urlencoded") + + r.Require().NoError(req.ParseForm()) + values := req.PostForm + r.Equal(values.Get("grant_type"), "client_credentials") + r.Equal(values.Get("client_id"), "client") + r.Equal(values.Get("client_secret"), "secret") + r.Equal(values.Get("scope"), "catalog") + + w.WriteHeader(http.StatusOK) + + json.NewEncoder(w).Encode(map[string]any{ + "access_token": TestToken, + "token_type": "Bearer", + "expires_in": 86400, + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, + catalog.WithWarehouseLocation("s3://some-bucket"), + catalog.WithCredential(TestCreds)) + r.Require().NoError(err) + + r.NotNil(cat) + r.Equal(r.configVals.Get("warehouse"), "s3://some-bucket") +} + +func (r *RestCatalogSuite) TestToken400() { + r.mux.HandleFunc("/v1/oauth/tokens", func(w http.ResponseWriter, req *http.Request) { + r.Equal(http.MethodPost, req.Method) + + r.Equal(req.Header.Get("Content-Type"), "application/x-www-form-urlencoded") + + w.WriteHeader(http.StatusBadRequest) + + json.NewEncoder(w).Encode(map[string]any{ + "error": "invalid_client", + "error_description": "credentials for key invalid_key do not match", + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithCredential(TestCreds)) + r.Nil(cat) + + r.ErrorIs(err, catalog.ErrRESTError) + r.ErrorIs(err, catalog.ErrOAuthError) + r.ErrorContains(err, "invalid_client: credentials for key invalid_key do not match") +} + +func (r *RestCatalogSuite) TestToken200AuthUrl() { + r.mux.HandleFunc("/auth-token-url", func(w http.ResponseWriter, req *http.Request) { + r.Equal(http.MethodPost, req.Method) + + r.Equal(req.Header.Get("Content-Type"), "application/x-www-form-urlencoded") + + r.Require().NoError(req.ParseForm()) + values := req.PostForm + r.Equal(values.Get("grant_type"), "client_credentials") + r.Equal(values.Get("client_id"), "client") + r.Equal(values.Get("client_secret"), "secret") + r.Equal(values.Get("scope"), "catalog") + + w.WriteHeader(http.StatusOK) + + json.NewEncoder(w).Encode(map[string]any{ + "access_token": TestToken, + "token_type": "Bearer", + "expires_in": 86400, + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + }) + }) + + authUri, err := url.Parse(r.srv.URL) + r.Require().NoError(err) + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, + catalog.WithWarehouseLocation("s3://some-bucket"), + catalog.WithCredential(TestCreds), catalog.WithAuthURI(authUri.JoinPath("auth-token-url"))) + + r.Require().NoError(err) + + r.NotNil(cat) + r.Equal(r.configVals.Get("warehouse"), "s3://some-bucket") +} + +func (r *RestCatalogSuite) TestToken401() { + r.mux.HandleFunc("/v1/oauth/tokens", func(w http.ResponseWriter, req *http.Request) { + r.Equal(http.MethodPost, req.Method) + + r.Equal(req.Header.Get("Content-Type"), "application/x-www-form-urlencoded") + + w.WriteHeader(http.StatusUnauthorized) + + json.NewEncoder(w).Encode(map[string]any{ + "error": "invalid_client", + "error_description": "credentials for key invalid_key do not match", + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithCredential(TestCreds)) + r.Nil(cat) + + r.ErrorIs(err, catalog.ErrRESTError) + r.ErrorIs(err, catalog.ErrOAuthError) + r.ErrorContains(err, "invalid_client: credentials for key invalid_key do not match") +} + +func (r *RestCatalogSuite) TestListTables200() { + namespace := "examples" + r.mux.HandleFunc("/v1/namespaces/"+namespace+"/tables", 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)) + } + + json.NewEncoder(w).Encode(map[string]any{ + "identifiers": []any{ + map[string]any{ + "namespace": []string{namespace}, + "name": "fooshare", + }, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + tables, err := cat.ListTables(context.Background(), catalog.ToRestIdentifier(namespace)) + r.Require().NoError(err) + r.Equal([]table.Identifier{{"examples", "fooshare"}}, tables) +} + +func (r *RestCatalogSuite) TestListTablesPrefixed200() { + r.mux.HandleFunc("/v1/oauth/tokens", func(w http.ResponseWriter, req *http.Request) { + r.Equal(http.MethodPost, req.Method) + + r.Equal(req.Header.Get("Content-Type"), "application/x-www-form-urlencoded") + + r.Require().NoError(req.ParseForm()) + values := req.PostForm + r.Equal(values.Get("grant_type"), "client_credentials") + r.Equal(values.Get("client_id"), "client") + r.Equal(values.Get("client_secret"), "secret") + r.Equal(values.Get("scope"), "catalog") + + w.WriteHeader(http.StatusOK) + + json.NewEncoder(w).Encode(map[string]any{ + "access_token": TestToken, + "token_type": "Bearer", + "expires_in": 86400, + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + }) + }) + + namespace := "examples" + r.mux.HandleFunc("/v1/prefix/namespaces/"+namespace+"/tables", 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)) + } + + json.NewEncoder(w).Encode(map[string]any{ + "identifiers": []any{ + map[string]any{ + "namespace": []string{namespace}, + "name": "fooshare", + }, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, + catalog.WithPrefix("prefix"), + catalog.WithWarehouseLocation("s3://some-bucket"), + catalog.WithCredential(TestCreds)) + r.Require().NoError(err) + + r.NotNil(cat) + r.Equal(r.configVals.Get("warehouse"), "s3://some-bucket") + + tables, err := cat.ListTables(context.Background(), catalog.ToRestIdentifier(namespace)) + r.Require().NoError(err) + r.Equal([]table.Identifier{{"examples", "fooshare"}}, tables) +} + +func (r *RestCatalogSuite) TestListTables404() { + namespace := "examples" + r.mux.HandleFunc("/v1/namespaces/"+namespace+"/tables", 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.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]any{ + "error": map[string]any{ + "message": "Namespace does not exist: personal in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e", + "type": "NoSuchNamespaceException", + "code": 404, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + _, err = cat.ListTables(context.Background(), catalog.ToRestIdentifier(namespace)) + r.ErrorIs(err, catalog.ErrNoSuchNamespace) + r.ErrorContains(err, "Namespace does not exist: personal in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e") +} + +func (r *RestCatalogSuite) TestListNamespaces200() { + r.mux.HandleFunc("/v1/namespaces", 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)) + } + + json.NewEncoder(w).Encode(map[string]any{ + "namespaces": []table.Identifier{ + {"default"}, {"examples"}, {"fokko"}, {"system"}, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + results, err := cat.ListNamespaces(context.Background(), nil) + r.Require().NoError(err) + + r.Equal([]table.Identifier{{"default"}, {"examples"}, {"fokko"}, {"system"}}, results) +} + +func (r *RestCatalogSuite) TestListNamespaceWithParent200() { + r.mux.HandleFunc("/v1/namespaces", func(w http.ResponseWriter, req *http.Request) { + r.Require().Equal(http.MethodGet, req.Method) + r.Require().Equal("accounting", req.URL.Query().Get("parent")) + + for k, v := range TestHeaders { + r.Equal(v, req.Header.Values(k)) + } + + json.NewEncoder(w).Encode(map[string]any{ + "namespaces": []table.Identifier{ + {"accounting", "tax"}, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + results, err := cat.ListNamespaces(context.Background(), catalog.ToRestIdentifier("accounting")) + r.Require().NoError(err) + + r.Equal([]table.Identifier{{"accounting", "tax"}}, results) +} + +func (r *RestCatalogSuite) TestListNamespaces400() { + r.mux.HandleFunc("/v1/namespaces", 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.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]any{ + "error": map[string]any{ + "message": "Namespace does not exist: personal in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e", + "type": "NoSuchNamespaceException", + "code": 404, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + _, err = cat.ListNamespaces(context.Background(), catalog.ToRestIdentifier("accounting")) + r.ErrorIs(err, catalog.ErrNoSuchNamespace) + r.ErrorContains(err, "Namespace does not exist: personal in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e") +} + +func (r *RestCatalogSuite) TestCreateNamespace200() { + r.mux.HandleFunc("/v1/namespaces", func(w http.ResponseWriter, req *http.Request) { + r.Require().Equal(http.MethodPost, req.Method) + r.Require().Equal("application/json", req.Header.Get("Content-Type")) + + for k, v := range TestHeaders { + r.Equal(v, req.Header.Values(k)) + } + + defer req.Body.Close() + dec := json.NewDecoder(req.Body) + body := struct { + Namespace table.Identifier `json:"namespace"` + Props iceberg.Properties `json:"properties"` + }{} + + r.Require().NoError(dec.Decode(&body)) + r.Equal(table.Identifier{"leden"}, body.Namespace) + r.Empty(body.Props) + + json.NewEncoder(w).Encode(map[string]any{ + "namespace": []string{"leden"}, "properties": map[string]any{}, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + r.Require().NoError(cat.CreateNamespace(context.Background(), catalog.ToRestIdentifier("leden"), nil)) +} + +func (r *RestCatalogSuite) TestCreateNamespaceWithProps200() { + r.mux.HandleFunc("/v1/namespaces", func(w http.ResponseWriter, req *http.Request) { + r.Require().Equal(http.MethodPost, req.Method) + r.Require().Equal("application/json", req.Header.Get("Content-Type")) + + for k, v := range TestHeaders { + r.Equal(v, req.Header.Values(k)) + } + + defer req.Body.Close() + dec := json.NewDecoder(req.Body) + body := struct { + Namespace table.Identifier `json:"namespace"` + Props iceberg.Properties `json:"properties"` + }{} + + r.Require().NoError(dec.Decode(&body)) + r.Equal(table.Identifier{"leden"}, body.Namespace) + r.Equal(iceberg.Properties{"foo": "bar", "super": "duper"}, body.Props) + + json.NewEncoder(w).Encode(map[string]any{ + "namespace": []string{"leden"}, "properties": body.Props, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + r.Require().NoError(cat.CreateNamespace(context.Background(), catalog.ToRestIdentifier("leden"), iceberg.Properties{"foo": "bar", "super": "duper"})) +} + +func (r *RestCatalogSuite) TestCreateNamespace409() { + r.mux.HandleFunc("/v1/namespaces", func(w http.ResponseWriter, req *http.Request) { + r.Require().Equal(http.MethodPost, req.Method) + r.Require().Equal("application/json", req.Header.Get("Content-Type")) + + for k, v := range TestHeaders { + r.Equal(v, req.Header.Values(k)) + } + + defer req.Body.Close() + dec := json.NewDecoder(req.Body) + body := struct { + Namespace table.Identifier `json:"namespace"` + Props iceberg.Properties `json:"properties"` + }{} + + r.Require().NoError(dec.Decode(&body)) + r.Equal(table.Identifier{"fokko"}, body.Namespace) + r.Empty(body.Props) + + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(map[string]any{ + "error": map[string]any{ + "message": "Namespace already exists: fokko in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e", + "type": "AlreadyExistsException", + "code": 409, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + err = cat.CreateNamespace(context.Background(), catalog.ToRestIdentifier("fokko"), nil) + r.ErrorIs(err, catalog.ErrNamespaceAlreadyExists) + r.ErrorContains(err, "fokko in warehouse") +} + +func (r *RestCatalogSuite) TestDropNamespace204() { + r.mux.HandleFunc("/v1/namespaces/examples", 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.StatusNoContent) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + r.NoError(cat.DropNamespace(context.Background(), catalog.ToRestIdentifier("examples"))) +} + +func (r *RestCatalogSuite) TestDropNamespace404() { + r.mux.HandleFunc("/v1/namespaces/examples", 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) + json.NewEncoder(w).Encode(map[string]any{ + "error": map[string]any{ + "message": "Namespace does not exist: examples in warehouse", + "type": "NoSuchNamespaceException", + "code": 404, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + err = cat.DropNamespace(context.Background(), catalog.ToRestIdentifier("examples")) + r.ErrorIs(err, catalog.ErrNoSuchNamespace) + r.ErrorContains(err, "examples in warehouse") +} + +func (r *RestCatalogSuite) TestLoadNamespaceProps200() { + r.mux.HandleFunc("/v1/namespaces/leden", 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.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]any{ + "namespace": []string{"fokko"}, + "properties": map[string]any{"prop": "yes"}, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + props, err := cat.LoadNamespaceProperties(context.Background(), catalog.ToRestIdentifier("leden")) + r.Require().NoError(err) + r.Equal(iceberg.Properties{"prop": "yes"}, props) +} + +func (r *RestCatalogSuite) TestLoadNamespaceProps404() { + r.mux.HandleFunc("/v1/namespaces/leden", 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.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]any{ + "error": map[string]any{ + "message": "Namespace does not exist: fokko22 in warehouse", + "type": "NoSuchNamespaceException", + "code": 404, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + _, err = cat.LoadNamespaceProperties(context.Background(), catalog.ToRestIdentifier("leden")) + r.ErrorIs(err, catalog.ErrNoSuchNamespace) + r.ErrorContains(err, "Namespace does not exist: fokko22 in warehouse") +} + +func (r *RestCatalogSuite) TestUpdateNamespaceProps200() { + r.mux.HandleFunc("/v1/namespaces/fokko/properties", 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)) + } + + json.NewEncoder(w).Encode(map[string]any{ + "removed": []string{}, + "updated": []string{"prop"}, + "missing": []string{"abc"}, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + summary, err := cat.UpdateNamespaceProperties(context.Background(), table.Identifier([]string{"fokko"}), + []string{"abc"}, iceberg.Properties{"prop": "yes"}) + r.Require().NoError(err) + + r.Equal(catalog.PropertiesUpdateSummary{ + Removed: []string{}, + Updated: []string{"prop"}, + Missing: []string{"abc"}, + }, summary) +} + +func (r *RestCatalogSuite) TestUpdateNamespaceProps404() { + r.mux.HandleFunc("/v1/namespaces/fokko/properties", 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.StatusNotFound) + json.NewEncoder(w).Encode(map[string]any{ + "error": map[string]any{ + "message": "Namespace does not exist: does_not_exist in warehouse", + "type": "NoSuchNamespaceException", + "code": 404, + }, + }) + }) + + cat, err := catalog.NewRestCatalog("rest", r.srv.URL, catalog.WithOAuthToken(TestToken)) + r.Require().NoError(err) + + _, err = cat.UpdateNamespaceProperties(context.Background(), + table.Identifier{"fokko"}, []string{"abc"}, iceberg.Properties{"prop": "yes"}) + r.ErrorIs(err, catalog.ErrNoSuchNamespace) + r.ErrorContains(err, "Namespace does not exist: does_not_exist in warehouse") +} + +func (r *RestCatalogSuite) TestLoadTable200() { + r.mux.HandleFunc("/v1/namespaces/fokko/tables/table", 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(`{ + "metadata-location": "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json", + "metadata": { + "format-version": 1, + "table-uuid": "b55d9dda-6561-423a-8bfc-787980ce421f", + "location": "s3://warehouse/database/table", + "last-updated-ms": 1646787054459, + "last-column-id": 2, + "schema": { + "type": "struct", + "schema-id": 0, + "fields": [ + {"id": 1, "name": "id", "required": false, "type": "int"}, + {"id": 2, "name": "data", "required": false, "type": "string"} + ] + }, + "current-schema-id": 0, + "schemas": [ + { + "type": "struct", + "schema-id": 0, + "fields": [ + {"id": 1, "name": "id", "required": false, "type": "int"}, + {"id": 2, "name": "data", "required": false, "type": "string"} + ] + } + ], + "partition-spec": [], + "default-spec-id": 0, + "partition-specs": [{"spec-id": 0, "fields": []}], + "last-partition-id": 999, + "default-sort-order-id": 0, + "sort-orders": [{"order-id": 0, "fields": []}], + "properties": {"owner": "bryan", "write.metadata.compression-codec": "gzip"}, + "current-snapshot-id": 3497810964824022504, + "refs": {"main": {"snapshot-id": 3497810964824022504, "type": "branch"}}, + "snapshots": [ + { + "snapshot-id": 3497810964824022504, + "timestamp-ms": 1646787054459, + "summary": { + "operation": "append", + "spark.app.id": "local-1646787004168", + "added-data-files": "1", + "added-records": "1", + "added-files-size": "697", + "changed-partition-count": "1", + "total-records": "1", + "total-files-size": "697", + "total-data-files": "1", + "total-delete-files": "0", + "total-position-deletes": "0", + "total-equality-deletes": "0" + }, + "manifest-list": "s3://warehouse/database/table/metadata/snap-3497810964824022504-1-c4f68204-666b-4e50-a9df-b10c34bf6b82.avro", + "schema-id": 0 + } + ], + "snapshot-log": [{"timestamp-ms": 1646787054459, "snapshot-id": 3497810964824022504}], + "metadata-log": [ + { + "timestamp-ms": 1646787031514, + "metadata-file": "s3://warehouse/database/table/metadata/00000-88484a1c-00e5-4a07-a787-c0e7aeffa805.gz.metadata.json" + } + ] + }, + "config": {"client.factory": "io.tabular.iceberg.catalog.TabularAwsClientFactory", "region": "us-west-2"} Review Comment: I don't think we need the `config` here as this isn't specific to OSS Iceberg -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: issues-unsubscr...@iceberg.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: issues-unsubscr...@iceberg.apache.org For additional commands, e-mail: issues-h...@iceberg.apache.org