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

(iceberg-go) branch main updated: feat (catalog/rest): Add create view for rest catalog (#376)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new cdb568d  feat (catalog/rest): Add create view for rest catalog (#376)
cdb568d is described below

commit cdb568df61c955c5b4ef2648086671955fe1b5e7
Author: Dao Thanh Tung <tt...@accountancy.smu.edu.sg>
AuthorDate: Tue Apr 8 17:49:02 2025 +0100

    feat (catalog/rest): Add create view for rest catalog (#376)
    
    Currently, I'm returning only the error instead of a iceberg view class
    instance. The reason is because the response return in `metadata` key is
    quite different from the table related operation and will require some
    significant work to refactor
    https://github.com/apache/iceberg-go/blob/main/table/metadata.go. Let me
    know if you want to pursue that direction :pray:
    
    ---------
    
    Signed-off-by: dttung2905 <tt...@accountancy.smu.edu.sg>
---
 README.md                 |   1 +
 catalog/catalog.go        |   1 +
 catalog/rest/rest.go      |  76 ++++++++++++++++++++++
 catalog/rest/rest_test.go | 156 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 234 insertions(+)

diff --git a/README.md b/README.md
index 9558a00..6fc4802 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@ $ cd iceberg-go/cmd/iceberg && go build .
 | Check Namespace Exists   |  X   |      |          |  X   |  X  |
 | Drop Namespace           |  X   |      |          |  X   |  X  |
 | Set Namespace Properties |  X   |      |          |  X   |  X  |
+| Create View              |  X   |      |          |      |     |
 | List View                |  X   |      |          |      |     |
 | Drop View                |  X   |      |          |      |     |
 | Check View Exists        |  X   |      |          |      |     |
diff --git a/catalog/catalog.go b/catalog/catalog.go
index 6c4a809..f2b842d 100644
--- a/catalog/catalog.go
+++ b/catalog/catalog.go
@@ -59,6 +59,7 @@ var (
 	ErrCatalogNotFound        = errors.New("catalog type not registered")
 	ErrNamespaceNotEmpty      = errors.New("namespace is not empty")
 	ErrNoSuchView             = errors.New("view does not exist")
+	ErrViewAlreadyExists      = errors.New("view already exists")
 )
 
 type PropertiesUpdateSummary struct {
diff --git a/catalog/rest/rest.go b/catalog/rest/rest.go
index ac86428..f6e66e7 100644
--- a/catalog/rest/rest.go
+++ b/catalog/rest/rest.go
@@ -1155,3 +1155,79 @@ func (r *Catalog) CheckViewExists(ctx context.Context, identifier table.Identifi
 
 	return true, nil
 }
+
+type viewVersion struct {
+	VersionID       int64             `json:"version-id"`
+	TimestampMs     int64             `json:"timestamp-ms"`
+	SchemaID        int               `json:"schema-id"`
+	Summary         map[string]string `json:"summary"`
+	Representations []struct {
+		Type    string `json:"type"`
+		SQL     string `json:"sql"`
+		Dialect string `json:"dialect"`
+	} `json:"representations"`
+	DefaultCatalog   string   `json:"default-catalog"`
+	DefaultNamespace []string `json:"default-namespace"`
+}
+
+type createViewRequest struct {
+	Name        string             `json:"name"`
+	Schema      *iceberg.Schema    `json:"schema"`
+	Location    string             `json:"location,omitempty"`
+	Props       iceberg.Properties `json:"properties,omitempty"`
+	SQL         string             `json:"sql"`
+	ViewVersion viewVersion        `json:"view-version"`
+}
+
+type viewResponse struct {
+	MetadataLoc string             `json:"metadata-location"`
+	RawMetadata json.RawMessage    `json:"metadata"`
+	Config      iceberg.Properties `json:"config"`
+	Metadata    table.Metadata     `json:"-"`
+}
+
+// CreateView creates a new view in the catalog.
+func (r *Catalog) CreateView(ctx context.Context, identifier table.Identifier, schema *iceberg.Schema, sql string, props iceberg.Properties) error {
+	ns, view, err := splitIdentForPath(identifier)
+	if err != nil {
+		return err
+	}
+
+	freshSchema, err := iceberg.AssignFreshSchemaIDs(schema, nil)
+	if err != nil {
+		return err
+	}
+
+	payload := createViewRequest{
+		Name:   view,
+		Schema: freshSchema,
+		SQL:    sql,
+		Props:  props,
+		ViewVersion: viewVersion{
+			VersionID:   1,
+			TimestampMs: time.Now().UnixMilli(),
+			SchemaID:    freshSchema.ID,
+			Summary:     map[string]string{"sql": sql},
+			Representations: []struct {
+				Type    string `json:"type"`
+				SQL     string `json:"sql"`
+				Dialect string `json:"dialect"`
+			}{
+				{Type: "sql", SQL: sql, Dialect: "default"},
+			},
+			DefaultCatalog:   r.name,
+			DefaultNamespace: strings.Split(ns, namespaceSeparator),
+		},
+	}
+
+	_, err = doPost[createViewRequest, viewResponse](ctx, r.baseURI, []string{"namespaces", ns, "views"}, payload,
+		r.cl, map[int]error{
+			http.StatusNotFound: catalog.ErrNoSuchNamespace,
+			http.StatusConflict: catalog.ErrViewAlreadyExists,
+		})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/catalog/rest/rest_test.go b/catalog/rest/rest_test.go
index 93ffebf..a6c836f 100644
--- a/catalog/rest/rest_test.go
+++ b/catalog/rest/rest_test.go
@@ -28,6 +28,7 @@ import (
 	"net/url"
 	"strconv"
 	"testing"
+	"time"
 
 	"github.com/apache/iceberg-go"
 	"github.com/apache/iceberg-go/catalog"
@@ -1956,3 +1957,158 @@ func TestRestCatalog(t *testing.T) {
 	suite.Run(t, new(RestCatalogSuite))
 	suite.Run(t, new(RestTLSCatalogSuite))
 }
+
+type errorResponse struct {
+	Message string `json:"message"`
+	Type    string `json:"type"`
+	Code    int    `json:"code"`
+}
+
+type createViewRequest struct {
+	Name        string             `json:"name"`
+	Schema      *iceberg.Schema    `json:"schema"`
+	SQL         string             `json:"sql"`
+	Props       iceberg.Properties `json:"properties"`
+	ViewVersion struct {
+		VersionID       int               `json:"version-id"`
+		TimestampMs     int64             `json:"timestamp-ms"`
+		SchemaID        int               `json:"schema-id"`
+		Summary         map[string]string `json:"summary"`
+		Representations []struct {
+			Type    string `json:"type"`
+			SQL     string `json:"sql"`
+			Dialect string `json:"dialect"`
+		} `json:"representations"`
+		DefaultCatalog   string   `json:"default-catalog"`
+		DefaultNamespace []string `json:"default-namespace"`
+	} `json:"view-version"`
+}
+
+type viewResponse struct {
+	MetadataLoc string             `json:"metadata-location"`
+	Config      iceberg.Properties `json:"config"`
+}
+
+func (r *RestCatalogSuite) TestCreateView200() {
+	ns := "ns"
+	view := "view"
+	identifier := table.Identifier{ns, view}
+	schema := iceberg.NewSchema(0, iceberg.NestedField{
+		ID:       1,
+		Name:     "id",
+		Type:     iceberg.PrimitiveTypes.Int32,
+		Required: true,
+	})
+	sql := "SELECT * FROM table"
+	viewVersionJSON, _ := json.Marshal(map[string]interface{}{
+		"version-id":   1,
+		"timestamp-ms": time.Now().UnixMilli(),
+		"schema-id":    schema.ID,
+		"summary":      map[string]string{"sql": sql},
+		"representations": []map[string]string{
+			{"type": "sql", "sql": sql, "dialect": "default"},
+		},
+		"default-catalog":   "default-catalog",
+		"default-namespace": []string{ns},
+	})
+	props := iceberg.Properties{
+		"comment":      "Example view created via REST catalog",
+		"owner":        "admin",
+		"view-version": string(viewVersionJSON),
+		"view-format":  "iceberg",
+		"view-sql":     sql,
+	}
+	r.mux.HandleFunc("/v1/namespaces/"+ns+"/views", func(w http.ResponseWriter, req *http.Request) {
+		r.Equal(http.MethodPost, req.Method)
+		r.Equal("application/json", req.Header.Get("Content-Type"))
+
+		var payload createViewRequest
+		err := json.NewDecoder(req.Body).Decode(&payload)
+		r.NoError(err)
+		r.Equal(view, payload.Name)
+		r.Equal(sql, payload.SQL)
+		r.Equal(schema.ID, payload.Schema.ID)
+		r.Equal(1, payload.ViewVersion.VersionID)
+		r.Equal(0, payload.ViewVersion.SchemaID)
+		r.Equal("sql", payload.ViewVersion.Representations[0].Type)
+		r.Equal(sql, payload.ViewVersion.Representations[0].SQL)
+		r.Equal("default", payload.ViewVersion.Representations[0].Dialect)
+		r.Equal("rest", payload.ViewVersion.DefaultCatalog)
+		r.Equal([]string{ns}, payload.ViewVersion.DefaultNamespace)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		json.NewEncoder(w).Encode(viewResponse{
+			MetadataLoc: "metadata-location",
+			Config:      iceberg.Properties{},
+		})
+	})
+
+	ctlg, err := rest.NewCatalog(context.Background(), "rest", r.srv.URL)
+	r.NoError(err)
+
+	err = ctlg.CreateView(context.Background(), identifier, schema, sql, props)
+	r.NoError(err)
+}
+
+func (r *RestCatalogSuite) TestCreateView409() {
+	ns := "ns"
+	view := "view"
+	identifier := table.Identifier{ns, view}
+	schema := iceberg.NewSchema(1, iceberg.NestedField{
+		ID:       1,
+		Name:     "id",
+		Type:     iceberg.PrimitiveTypes.Int32,
+		Required: true,
+	})
+	sql := "SELECT * FROM table"
+
+	r.mux.HandleFunc("/v1/namespaces/"+ns+"/views", func(w http.ResponseWriter, req *http.Request) {
+		r.Equal(http.MethodPost, req.Method)
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusConflict)
+		json.NewEncoder(w).Encode(errorResponse{
+			Message: "The given view already exists",
+			Type:    "AlreadyExistsException",
+			Code:    409,
+		})
+	})
+
+	ctlg, err := rest.NewCatalog(context.Background(), "rest", r.srv.URL)
+	r.NoError(err)
+
+	err = ctlg.CreateView(context.Background(), identifier, schema, sql, nil)
+	r.Error(err)
+	r.ErrorIs(err, catalog.ErrViewAlreadyExists)
+}
+
+func (r *RestCatalogSuite) TestCreateView404() {
+	ns := "ns"
+	view := "view"
+	identifier := table.Identifier{ns, view}
+	schema := iceberg.NewSchema(1, iceberg.NestedField{
+		ID:       1,
+		Name:     "id",
+		Type:     iceberg.PrimitiveTypes.Int32,
+		Required: true,
+	})
+	sql := "SELECT * FROM table"
+
+	r.mux.HandleFunc("/v1/namespaces/"+ns+"/views", func(w http.ResponseWriter, req *http.Request) {
+		r.Equal(http.MethodPost, req.Method)
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNotFound)
+		json.NewEncoder(w).Encode(errorResponse{
+			Message: "namespace does not exist",
+			Type:    "NoSuchNamespaceException",
+			Code:    404,
+		})
+	})
+
+	ctlg, err := rest.NewCatalog(context.Background(), "rest", r.srv.URL)
+	r.NoError(err)
+
+	err = ctlg.CreateView(context.Background(), identifier, schema, sql, nil)
+	r.Error(err)
+	r.ErrorIs(err, catalog.ErrNoSuchNamespace)
+}