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)
+}