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

jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new abc2cd2  fix(console): Allow the console to show Iceberg Views. (#116)
abc2cd2 is described below

commit abc2cd23b49882e17f49fb0ac1a6f88e779b0768
Author: Adam Christian 
<[email protected]>
AuthorDate: Sat Jan 10 01:48:11 2026 -0500

    fix(console): Allow the console to show Iceberg Views. (#116)
    
    * fix(console): Allow the console to show Iceberg Views.
    
    * fix: Resolve TypeScript and React hooks linting errors
    
    - Move useMutation hook before early return to comply with React hooks rules
    - Replace enum with const object to fix erasableSyntaxOnly error
    - Rename duplicate SchemaField interface to ViewSchemaField to avoid type 
conflict
    
    ---------
    
    Co-authored-by: Adam Christian <[email protected]>
---
 console/src/api/catalog/views.ts                   | 110 +++++++
 console/src/app.tsx                                |   2 +
 console/src/components/catalog/CatalogExplorer.tsx |  48 ++-
 console/src/components/catalog/CatalogTreeNode.tsx |  50 ++-
 .../src/components/catalog/ViewDetailsDrawer.tsx   | 229 ++++++++++++++
 console/src/components/forms/CreateViewModal.tsx   | 199 ++++++++++++
 console/src/pages/NamespaceDetails.tsx             | 111 ++++++-
 console/src/pages/ViewDetails.tsx                  | 350 +++++++++++++++++++++
 console/src/types/api.ts                           |  67 ++++
 9 files changed, 1152 insertions(+), 14 deletions(-)

diff --git a/console/src/api/catalog/views.ts b/console/src/api/catalog/views.ts
new file mode 100644
index 0000000..30aa7d1
--- /dev/null
+++ b/console/src/api/catalog/views.ts
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+import { apiClient } from "../client"
+import type { ListViewsResponse, CreateViewRequest, LoadViewResult } from 
"@/types/api"
+
+/**
+ * Encodes a namespace array to URL format.
+ * Namespace parts are separated by the unit separator character (0x1F).
+ */
+function encodeNamespace(namespace: string[]): string {
+  return namespace.join("\x1F")
+}
+
+export const viewsApi = {
+  /**
+   * List views in a namespace.
+   * @param prefix - The catalog name (prefix)
+   * @param namespace - Namespace array (e.g., ["accounting", "tax"])
+   */
+  list: async (
+    prefix: string,
+    namespace: string[]
+  ): Promise<Array<{ namespace: string[]; name: string }>> => {
+    const namespaceStr = encodeNamespace(namespace)
+    const response = await apiClient
+      .getCatalogClient()
+      .get<ListViewsResponse>(
+        
`/${encodeURIComponent(prefix)}/namespaces/${encodeURIComponent(namespaceStr)}/views`
+      )
+    return response.data.identifiers
+  },
+
+  /**
+   * Get view details.
+   * @param prefix - The catalog name
+   * @param namespace - Namespace array (e.g., ["accounting", "tax"])
+   * @param viewName - View name
+   */
+  get: async (
+    prefix: string,
+    namespace: string[],
+    viewName: string
+  ): Promise<LoadViewResult> => {
+    const namespaceStr = encodeNamespace(namespace)
+    const response = await apiClient
+      .getCatalogClient()
+      .get<LoadViewResult>(
+        
`/${encodeURIComponent(prefix)}/namespaces/${encodeURIComponent(namespaceStr)}/views/${encodeURIComponent(viewName)}`
+      )
+    return response.data
+  },
+
+  /**
+   * Delete a view.
+   * @param prefix - The catalog name
+   * @param namespace - Namespace array
+   * @param viewName - View name
+   */
+  delete: async (
+    prefix: string,
+    namespace: string[],
+    viewName: string
+  ): Promise<void> => {
+    const namespaceStr = encodeNamespace(namespace)
+    await apiClient
+      .getCatalogClient()
+      .delete(
+        
`/${encodeURIComponent(prefix)}/namespaces/${encodeURIComponent(namespaceStr)}/views/${encodeURIComponent(viewName)}`
+      )
+  },
+
+  /**
+   * Create a view in a namespace.
+   * @param prefix - The catalog name
+   * @param namespace - Namespace array
+   * @param request - Create view request body
+   */
+  create: async (
+    prefix: string,
+    namespace: string[],
+    request: CreateViewRequest
+  ): Promise<LoadViewResult> => {
+    const namespaceStr = encodeNamespace(namespace)
+    const response = await apiClient
+      .getCatalogClient()
+      .post<LoadViewResult>(
+        
`/${encodeURIComponent(prefix)}/namespaces/${encodeURIComponent(namespaceStr)}/views`,
+        request
+      )
+    return response.data
+  },
+}
+
diff --git a/console/src/app.tsx b/console/src/app.tsx
index 3446a1f..78a45e2 100644
--- a/console/src/app.tsx
+++ b/console/src/app.tsx
@@ -33,6 +33,7 @@ import { CatalogDetails } from "@/pages/CatalogDetails"
 import { NamespaceDetails } from "@/pages/NamespaceDetails"
 import { AccessControl } from "@/pages/AccessControl"
 import { TableDetails } from "@/pages/TableDetails"
+import { ViewDetails } from "@/pages/ViewDetails"
 
 function ThemedToaster() {
   const { effectiveTheme } = useTheme()
@@ -70,6 +71,7 @@ function App() {
                   <Route path="/catalogs/:catalogName" 
element={<CatalogDetails />} />
                   <Route path="/catalogs/:catalogName/namespaces/:namespace" 
element={<NamespaceDetails />} />
                   <Route 
path="/catalogs/:catalogName/namespaces/:namespace/tables/:tableName" 
element={<TableDetails />} />
+                  <Route 
path="/catalogs/:catalogName/namespaces/:namespace/views/:viewName" 
element={<ViewDetails />} />
                   <Route path="/access-control" element={<AccessControl />} />
                 </Route>
                 <Route path="*" element={<Navigate to="/" replace />} />
diff --git a/console/src/components/catalog/CatalogExplorer.tsx 
b/console/src/components/catalog/CatalogExplorer.tsx
index e1c3b58..11670c0 100644
--- a/console/src/components/catalog/CatalogExplorer.tsx
+++ b/console/src/components/catalog/CatalogExplorer.tsx
@@ -25,6 +25,7 @@ import { cn } from "@/lib/utils"
 import { CatalogTreeNode, type TreeNode } from "./CatalogTreeNode"
 import { catalogsApi } from "@/api/management/catalogs"
 import { TableDetailsDrawer } from "./TableDetailsDrawer"
+import { ViewDetailsDrawer } from "./ViewDetailsDrawer"
 import { useResizableWidth } from "@/hooks/useResizableWidth"
 import { CATALOG_NODE_PREFIX } from "@/lib/constants"
 
@@ -34,10 +35,18 @@ interface CatalogExplorerProps {
   className?: string
 }
 
-interface SelectedTable {
+const CatalogEntityType = {
+    TABLE: "table",
+    VIEW: "view",
+} as const
+
+type CatalogEntityType = typeof CatalogEntityType[keyof typeof 
CatalogEntityType]
+
+interface SelectedCatalogEntity {
   catalogName: string
   namespace: string[]
-  tableName: string
+  name: string
+  type: CatalogEntityType
 }
 
 export function CatalogExplorer({
@@ -49,7 +58,7 @@ export function CatalogExplorer({
   const [selectedNodeId, setSelectedNodeId] = useState<string>()
   const [isCollapsed, setIsCollapsed] = useState(false)
   const [drawerOpen, setDrawerOpen] = useState(false)
-  const [selectedTable, setSelectedTable] = useState<SelectedTable | 
null>(null)
+  const [selectedCatalogEntity, setSelectedCatalogEntity] = 
useState<SelectedCatalogEntity | null>(null)
 
   // Use custom hook for resizable width
   const { width, isResizing, handleMouseDown } = useResizableWidth()
@@ -83,9 +92,18 @@ export function CatalogExplorer({
   const handleTableClick = useCallback((
     catalogName: string,
     namespace: string[],
-    tableName: string
+    name: string
+  ) => {
+    setSelectedCatalogEntity({ catalogName, namespace, name, type: 
CatalogEntityType.TABLE })
+    setDrawerOpen(true)
+  }, [])
+
+  const handleViewClick = useCallback((
+    catalogName: string,
+    namespace: string[],
+    name: string
   ) => {
-    setSelectedTable({ catalogName, namespace, tableName })
+    setSelectedCatalogEntity({ catalogName, namespace, name, type: 
CatalogEntityType.VIEW })
     setDrawerOpen(true)
   }, [])
 
@@ -179,6 +197,7 @@ export function CatalogExplorer({
                   onToggleExpand={handleToggleExpand}
                   onSelectNode={handleSelectNode}
                   onTableClick={handleTableClick}
+                  onViewClick={handleViewClick}
                 />
               ))}
             </div>
@@ -214,15 +233,26 @@ export function CatalogExplorer({
       )}
 
       {/* Table Details Drawer */}
-      {selectedTable && (
+      {selectedCatalogEntity && selectedCatalogEntity.type === 
CatalogEntityType.TABLE && (
         <TableDetailsDrawer
           open={drawerOpen}
           onOpenChange={setDrawerOpen}
-          catalogName={selectedTable.catalogName}
-          namespace={selectedTable.namespace}
-          tableName={selectedTable.tableName}
+          catalogName={selectedCatalogEntity.catalogName}
+          namespace={selectedCatalogEntity.namespace}
+          tableName={selectedCatalogEntity.name}
         />
       )}
+
+        {/* View Details Drawer */}
+      {selectedCatalogEntity && selectedCatalogEntity.type === 
CatalogEntityType.VIEW && (
+          <ViewDetailsDrawer
+            open={drawerOpen}
+            onOpenChange={setDrawerOpen}
+            catalogName={selectedCatalogEntity.catalogName}
+            namespace={selectedCatalogEntity.namespace}
+            viewName={selectedCatalogEntity.name}
+          />
+        )}
     </>
   )
 }
diff --git a/console/src/components/catalog/CatalogTreeNode.tsx 
b/console/src/components/catalog/CatalogTreeNode.tsx
index 6682330..d669e25 100644
--- a/console/src/components/catalog/CatalogTreeNode.tsx
+++ b/console/src/components/catalog/CatalogTreeNode.tsx
@@ -31,9 +31,10 @@ import {
 import { cn } from "@/lib/utils"
 import { namespacesApi } from "@/api/catalog/namespaces"
 import { tablesApi } from "@/api/catalog/tables"
+import { viewsApi } from "@/api/catalog/views"
 import type { Catalog } from "@/types/api"
 
-export type TreeNodeType = "catalog" | "namespace" | "table"
+export type TreeNodeType = "catalog" | "namespace" | "table" | "view"
 
 export interface TreeNode {
   type: TreeNodeType
@@ -52,6 +53,7 @@ interface CatalogTreeNodeProps {
   onToggleExpand: (nodeId: string) => void
   onSelectNode?: (node: TreeNode) => void
   onTableClick?: (catalogName: string, namespace: string[], tableName: string) 
=> void
+  onViewClick?: (catalogName: string, namespace: string[], viewName: string) 
=> void
 }
 
 export function CatalogTreeNode({
@@ -62,6 +64,7 @@ export function CatalogTreeNode({
   onToggleExpand,
   onSelectNode,
   onTableClick,
+  onViewClick,
 }: CatalogTreeNodeProps) {
   const isExpanded = expandedNodes.has(node.id)
   const isSelected = selectedNodeId === node.id
@@ -121,6 +124,22 @@ export function CatalogTreeNode({
       currentNamespacePath.length > 0,
   })
 
+  // Fetch views when namespace is expanded
+  const viewsQuery = useQuery({
+    queryKey: [
+      "views",
+      node.catalogName || "",
+      currentNamespacePath.join(".") || "",
+    ],
+    queryFn: () =>
+      viewsApi.list(node.catalogName || "", currentNamespacePath),
+    enabled:
+      node.type === "namespace" &&
+      isExpanded &&
+      !!node.catalogName &&
+      currentNamespacePath.length > 0,
+  })
+
   // Fetch generic tables when namespace is expanded
   const genericTablesQuery = useQuery({
     queryKey: [
@@ -152,6 +171,12 @@ export function CatalogTreeNode({
         onTableClick?.(node.catalogName, node.namespace, node.name)
       }
       onSelectNode?.(node)
+    } else if (node.type === "view") {
+      // Open view details when clicking a view
+      if (node.catalogName && node.namespace && node.namespace.length > 0) {
+        onViewClick?.(node.catalogName, node.namespace, node.name)
+      }
+      onSelectNode?.(node)
     }
   }
 
@@ -261,6 +286,21 @@ export function CatalogTreeNode({
           parent: node,
         })
       })
+
+      // Add views under namespace
+      // Views API returns identifiers like [{namespace: ["accounting"], name: 
"sales_view"}]
+      const views = viewsQuery.data || []
+      views.forEach((view) => {
+        const namespaceId = `${node.id}.view.${view.name}`
+        children.push({
+          type: "view",
+          id: namespaceId,
+          name: view.name,
+          namespace: currentNamespacePath, // Full namespace path where view 
resides
+          catalogName: node.catalogName,
+          parent: node,
+        })
+      })
     }
 
     return children
@@ -269,6 +309,7 @@ export function CatalogTreeNode({
     namespacesQuery.data,
     childNamespacesQuery.data,
     tablesQuery.data,
+    viewsQuery.data,
     genericTablesQuery.data,
     currentNamespacePath,
   ])
@@ -276,7 +317,7 @@ export function CatalogTreeNode({
   const isLoading =
     (node.type === "catalog" && namespacesQuery.isLoading) ||
     (node.type === "namespace" &&
-      (childNamespacesQuery.isLoading || tablesQuery.isLoading || 
genericTablesQuery.isLoading))
+      (childNamespacesQuery.isLoading || tablesQuery.isLoading || 
viewsQuery.isLoading || genericTablesQuery.isLoading))
 
   const Icon = useMemo(() => {
     if (node.type === "catalog") return Database
@@ -327,11 +368,12 @@ export function CatalogTreeNode({
                   onToggleExpand={onToggleExpand}
                   onSelectNode={onSelectNode}
                   onTableClick={onTableClick}
+                  onViewClick={onViewClick}
                 />
               ))}
-          {!isLoading && childNodes.length === 0 && node.type !== "table" && (
+          {!isLoading && childNodes.length === 0 && node.type !== "table" && 
node.type !== "view" && (
             <div className="px-2 py-1 text-xs text-muted-foreground italic">
-              No {node.type === "catalog" ? "namespaces" : "tables"} found
+              No {node.type === "catalog" ? "namespaces" : "items"} found
             </div>
           )}
         </div>
diff --git a/console/src/components/catalog/ViewDetailsDrawer.tsx 
b/console/src/components/catalog/ViewDetailsDrawer.tsx
new file mode 100644
index 0000000..91db8da
--- /dev/null
+++ b/console/src/components/catalog/ViewDetailsDrawer.tsx
@@ -0,0 +1,229 @@
+/*
+ * 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.
+ */
+
+import { useQuery } from "@tanstack/react-query"
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetHeader,
+  SheetTitle,
+} from "@/components/ui/sheet"
+import { viewsApi } from "@/api/catalog/views"
+import { Loader2 } from "lucide-react"
+import { TableSchemaDisplay } from "./TableSchemaDisplay"
+
+interface ViewDetailsDrawerProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  catalogName: string
+  namespace: string[]
+  viewName: string
+}
+
+export function ViewDetailsDrawer({
+  open,
+  onOpenChange,
+  catalogName,
+  namespace,
+  viewName,
+}: ViewDetailsDrawerProps) {
+  const viewQuery = useQuery({
+    queryKey: ["view", catalogName, namespace.join("."), viewName],
+    queryFn: () => viewsApi.get(catalogName, namespace, viewName),
+    enabled: open && !!catalogName && namespace.length > 0 && !!viewName,
+  })
+
+  const viewData = viewQuery.data
+
+  const currentVersion = viewData?.metadata?.versions?.find(
+    (v) => v["version-id"] === viewData?.metadata?.["current-version-id"]
+  )
+
+  const currentSchema = viewData?.metadata?.schemas?.find(
+    (s) => s["schema-id"] === currentVersion?.["schema-id"]
+  ) || viewData?.metadata?.schemas?.[0]
+
+  const currentSql = currentVersion?.representations?.find(
+    (r) => r.type === "sql"
+  )
+
+  return (
+    <Sheet open={open} onOpenChange={onOpenChange}>
+      <SheetContent side="right" className="w-full sm:max-w-2xl 
overflow-y-auto">
+        <SheetHeader>
+          <SheetTitle className="text-xl font-bold">
+            {viewName}
+          </SheetTitle>
+          <SheetDescription>
+            {namespace.length > 0 ? (
+              <span className="text-sm text-muted-foreground">
+                {catalogName}.{namespace.join(".")}.{viewName}
+              </span>
+            ) : (
+              <span className="text-sm text-muted-foreground">
+                {catalogName}.{viewName}
+              </span>
+            )}
+          </SheetDescription>
+        </SheetHeader>
+
+        {viewQuery.isLoading && (
+          <div className="flex items-center justify-center py-12">
+            <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
+          </div>
+        )}
+
+        {viewQuery.isError && (
+          <div className="py-12">
+            <div className="text-sm text-destructive">
+              Failed to load view details
+            </div>
+          </div>
+        )}
+
+        {viewData && (
+          <div className="mt-6 space-y-6">
+            {/* View Info */}
+            <div>
+              <h3 className="text-sm font-semibold mb-2">View Information</h3>
+              <div className="space-y-1 text-sm">
+                <div className="flex justify-between">
+                  <span className="text-muted-foreground">UUID:</span>
+                  <span className="font-mono text-xs">
+                    {viewData.metadata["view-uuid"]}
+                  </span>
+                </div>
+                <div className="flex justify-between">
+                  <span className="text-muted-foreground">Format 
Version:</span>
+                  <span>{viewData.metadata["format-version"]}</span>
+                </div>
+                <div className="flex justify-between">
+                  <span className="text-muted-foreground">Current 
Version:</span>
+                  <span>{viewData.metadata["current-version-id"]}</span>
+                </div>
+                {currentSql && (
+                  <div className="flex justify-between">
+                    <span className="text-muted-foreground">SQL Dialect:</span>
+                    <span>{currentSql.dialect}</span>
+                  </div>
+                )}
+                {viewData.metadata.location && (
+                  <div className="flex justify-between">
+                    <span className="text-muted-foreground">Location:</span>
+                    <span className="font-mono text-xs break-all">
+                      {viewData.metadata.location}
+                    </span>
+                  </div>
+                )}
+                {viewData["metadata-location"] && (
+                  <div className="flex justify-between">
+                    <span className="text-muted-foreground">
+                      Metadata Location:
+                    </span>
+                    <span className="font-mono text-xs break-all">
+                      {viewData["metadata-location"]}
+                    </span>
+                  </div>
+                )}
+              </div>
+            </div>
+
+            {/* SQL Query */}
+            {currentSql && (
+              <div>
+                <h3 className="text-sm font-semibold mb-2">SQL Query</h3>
+                <pre className="bg-muted p-3 rounded-md overflow-x-auto 
text-xs font-mono whitespace-pre-wrap">
+                  {currentSql.sql}
+                </pre>
+              </div>
+            )}
+
+            {/* Schema */}
+            {currentSchema && (
+              <TableSchemaDisplay schema={currentSchema} />
+            )}
+
+            {/* Properties */}
+            {viewData.metadata.properties &&
+              Object.keys(viewData.metadata.properties).length > 0 && (
+                <div>
+                  <h3 className="text-sm font-semibold mb-2">Properties</h3>
+                  <div className="border rounded-md">
+                    <div className="divide-y">
+                      {Object.entries(viewData.metadata.properties).map(
+                        ([key, value]) => (
+                          <div
+                            key={key}
+                            className="px-3 py-2 flex justify-between text-sm"
+                          >
+                            <span 
className="text-muted-foreground">{key}:</span>
+                            <span className="font-mono text-xs break-all">
+                              {String(value)}
+                            </span>
+                          </div>
+                        )
+                      )}
+                    </div>
+                  </div>
+                </div>
+              )}
+
+            {/* Version History */}
+            {viewData.metadata.versions && viewData.metadata.versions.length > 
0 && (
+              <div>
+                <h3 className="text-sm font-semibold mb-2">Version History</h3>
+                <div className="space-y-2">
+                  {viewData.metadata.versions.map((version) => {
+                    const sqlRep = version.representations?.find((r) => r.type 
=== "sql")
+                    const isCurrent = version["version-id"] === 
viewData.metadata["current-version-id"]
+                    return (
+                      <div
+                        key={version["version-id"]}
+                        className={`border rounded-md p-3 text-sm ${isCurrent 
? "border-primary bg-primary/5" : ""}`}
+                      >
+                        <div className="flex items-center justify-between 
mb-1">
+                          <span className="font-medium">
+                            Version {version["version-id"]}
+                            {isCurrent && (
+                              <span className="ml-2 text-xs 
text-primary">(current)</span>
+                            )}
+                          </span>
+                          <span className="text-xs text-muted-foreground">
+                            {new 
Date(version["timestamp-ms"]).toLocaleString()}
+                          </span>
+                        </div>
+                        {sqlRep && (
+                          <div className="text-xs">
+                            <span 
className="text-muted-foreground">Dialect:</span> {sqlRep.dialect}
+                          </div>
+                        )}
+                      </div>
+                    )
+                  })}
+                </div>
+              </div>
+            )}
+          </div>
+        )}
+      </SheetContent>
+    </Sheet>
+  )
+}
+
diff --git a/console/src/components/forms/CreateViewModal.tsx 
b/console/src/components/forms/CreateViewModal.tsx
new file mode 100644
index 0000000..c73a845
--- /dev/null
+++ b/console/src/components/forms/CreateViewModal.tsx
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+import { useForm } from "react-hook-form"
+import { z } from "zod"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useMutation } from "@tanstack/react-query"
+import { toast } from "sonner"
+import { viewsApi } from "@/api/catalog/views"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog"
+import type { CreateViewRequest } from "@/types/api"
+
+const schema = z.object({
+  name: z
+    .string()
+    .min(1, "View name is required")
+    .regex(
+      /^[a-zA-Z_][a-zA-Z0-9_]*$/,
+      "View name must start with a letter or underscore and contain only 
alphanumeric characters and underscores"
+    ),
+  sql: z.string().min(1, "SQL query is required"),
+  dialect: z.string().min(1, "SQL dialect is required"),
+})
+
+type FormValues = z.infer<typeof schema>
+
+interface CreateViewModalProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  catalogName: string
+  namespace: string[]
+  onCreated?: () => void
+}
+
+export function CreateViewModal({
+  open,
+  onOpenChange,
+  catalogName,
+  namespace,
+  onCreated,
+}: CreateViewModalProps) {
+  const {
+    register,
+    handleSubmit,
+    reset,
+    formState: { errors },
+  } = useForm<FormValues>({
+    resolver: zodResolver(schema),
+    defaultValues: {
+      name: "",
+      sql: "",
+      dialect: "spark",
+    },
+  })
+
+  const createMutation = useMutation({
+    mutationFn: (values: FormValues) => {
+      const request: CreateViewRequest = {
+        name: values.name,
+        schema: {
+          type: "struct",
+          fields: [],
+        },
+        "view-version": {
+          "version-id": 1,
+          "timestamp-ms": Date.now(),
+          "schema-id": 0,
+          summary: {
+            "engine-name": values.dialect,
+            "engine-version": "1.0.0",
+          },
+          representations: [
+            {
+              type: "sql",
+              sql: values.sql,
+              dialect: values.dialect,
+            },
+          ],
+          "default-namespace": namespace,
+        },
+        properties: {},
+      }
+      return viewsApi.create(catalogName, namespace, request)
+    },
+    onSuccess: () => {
+      toast.success("Iceberg view created successfully")
+      onOpenChange(false)
+      reset()
+      onCreated?.()
+    },
+    onError: (error: Error) => {
+      toast.error("Failed to create Iceberg view", {
+        description: error.message || "An error occurred",
+      })
+    },
+  })
+
+  const onSubmit = (values: FormValues) => {
+    createMutation.mutate(values)
+  }
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-[600px]">
+        <DialogHeader>
+          <DialogTitle>Create Iceberg View</DialogTitle>
+          <DialogDescription>
+            Create a new Iceberg view in the namespace "{namespace.join(".")}".
+          </DialogDescription>
+        </DialogHeader>
+        <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
+          <div className="space-y-2">
+            <Label htmlFor="name">View Name</Label>
+            <Input
+              id="name"
+              placeholder="my_view"
+              {...register("name")}
+            />
+            {errors.name && (
+              <p className="text-sm text-red-600">{errors.name.message}</p>
+            )}
+          </div>
+
+          <div className="space-y-2">
+            <Label htmlFor="dialect">SQL Dialect</Label>
+            <Input
+              id="dialect"
+              placeholder="spark"
+              {...register("dialect")}
+            />
+            {errors.dialect && (
+              <p className="text-sm text-red-600">{errors.dialect.message}</p>
+            )}
+            <p className="text-xs text-muted-foreground">
+              The SQL dialect used for the view (e.g., spark, trino, presto)
+            </p>
+          </div>
+
+          <div className="space-y-2">
+            <Label htmlFor="sql">SQL Query</Label>
+            <Textarea
+              id="sql"
+              placeholder="SELECT * FROM my_table WHERE ..."
+              className="min-h-[150px] font-mono text-sm"
+              {...register("sql")}
+            />
+            {errors.sql && (
+              <p className="text-sm text-red-600">{errors.sql.message}</p>
+            )}
+            <p className="text-xs text-muted-foreground">
+              The SQL query that defines this view
+            </p>
+          </div>
+
+          <DialogFooter>
+            <Button
+              type="button"
+              variant="outline"
+              onClick={() => onOpenChange(false)}
+            >
+              Cancel
+            </Button>
+            <Button type="submit" disabled={createMutation.isPending}>
+              {createMutation.isPending ? "Creating..." : "Create View"}
+            </Button>
+          </DialogFooter>
+        </form>
+      </DialogContent>
+    </Dialog>
+  )
+}
+
diff --git a/console/src/pages/NamespaceDetails.tsx 
b/console/src/pages/NamespaceDetails.tsx
index 8aeda8d..01c8d10 100644
--- a/console/src/pages/NamespaceDetails.tsx
+++ b/console/src/pages/NamespaceDetails.tsx
@@ -23,6 +23,7 @@ import { toast } from "sonner"
 import { ArrowLeft, Folder, Table as TableIcon, RefreshCw, Trash2, Plus } from 
"lucide-react"
 import { namespacesApi } from "@/api/catalog/namespaces"
 import { tablesApi } from "@/api/catalog/tables"
+import { viewsApi } from "@/api/catalog/views"
 import { catalogsApi } from "@/api/management/catalogs"
 import type { GenericTableIdentifier } from "@/types/api"
 import { Button } from "@/components/ui/button"
@@ -46,6 +47,7 @@ import {
 import { Input } from "@/components/ui/input"
 import { Label } from "@/components/ui/label"
 import { useState } from "react"
+import { CreateViewModal } from "@/components/forms/CreateViewModal"
 
 export function NamespaceDetails() {
   const { catalogName, namespace: namespaceParam } = useParams<{
@@ -56,6 +58,7 @@ export function NamespaceDetails() {
   const queryClient = useQueryClient()
   const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
   const [createGenericTableOpen, setCreateGenericTableOpen] = useState(false)
+  const [createViewOpen, setCreateViewOpen] = useState(false)
   const [genericTableForm, setGenericTableForm] = useState({
     name: "",
     format: "",
@@ -96,6 +99,12 @@ export function NamespaceDetails() {
     enabled: !!catalogName && namespaceArray.length > 0,
   })
 
+  const viewsQuery = useQuery({
+    queryKey: ["views", catalogName, namespaceArray],
+    queryFn: () => viewsApi.list(catalogName!, namespaceArray),
+    enabled: !!catalogName && namespaceArray.length > 0,
+  })
+
   const deleteMutation = useMutation({
     mutationFn: () => namespacesApi.delete(catalogName!, namespaceArray),
     onSuccess: () => {
@@ -149,6 +158,7 @@ export function NamespaceDetails() {
   const namespace = namespaceQuery.data
   const tables = tablesQuery.data ?? []
   const childrenNamespaces = childrenNamespacesQuery.data ?? []
+  const views = viewsQuery.data ?? []
 
   const handleTableClick = (tableName: string) => {
     if (!catalogName || !namespaceParam) return
@@ -165,6 +175,13 @@ export function NamespaceDetails() {
     )
   }
 
+  const handleViewClick = (viewName: string) => {
+    if (!catalogName || !namespaceParam) return
+    navigate(
+      
`/catalogs/${encodeURIComponent(catalogName)}/namespaces/${encodeURIComponent(namespaceParam)}/views/${encodeURIComponent(viewName)}`
+    )
+  }
+
   const handleDelete = () => {
     deleteMutation.mutate()
   }
@@ -203,8 +220,9 @@ export function NamespaceDetails() {
               catalogQuery.refetch()
               childrenNamespacesQuery.refetch()
               polarisGenericTablesQuery.refetch()
+              viewsQuery.refetch()
             }}
-            disabled={namespaceQuery.isFetching || tablesQuery.isFetching || 
childrenNamespacesQuery.isFetching || polarisGenericTablesQuery.isFetching}
+            disabled={namespaceQuery.isFetching || tablesQuery.isFetching || 
childrenNamespacesQuery.isFetching || polarisGenericTablesQuery.isFetching || 
viewsQuery.isFetching}
           >
             <RefreshCw className="mr-2 h-4 w-4" />
             Refresh
@@ -408,6 +426,86 @@ export function NamespaceDetails() {
         </>
       )}
 
+        {/* Iceberg Views Section */}
+        <Card>
+          <CardHeader>
+            <div className="flex items-center justify-between">
+              <div>
+                <CardTitle>Iceberg Views</CardTitle>
+                <CardDescription>
+                  Iceberg views in this namespace
+                </CardDescription>
+              </div>
+              <Button
+                onClick={() => setCreateViewOpen(true)}
+                disabled={!catalogName || !namespaceParam}
+              >
+                <Plus className="mr-2 h-4 w-4" />
+                Iceberg View
+              </Button>
+            </div>
+          </CardHeader>
+          <CardContent>
+            {viewsQuery.isLoading ? (
+              <div>Loading Iceberg views...</div>
+            ) : viewsQuery.error ? (
+              <div className="text-red-600">
+                Error loading Iceberg views: {viewsQuery.error.message}
+              </div>
+            ) : views.length === 0 ? (
+              <div className="text-center text-muted-foreground py-8">
+                No Iceberg views found in this namespace.
+              </div>
+            ) : (
+              <div className="rounded-md border">
+                <Table>
+                  <TableHeader>
+                    <TableRow>
+                      <TableHead>Name</TableHead>
+                      <TableHead>Namespace</TableHead>
+                      <TableHead>Actions</TableHead>
+                    </TableRow>
+                  </TableHeader>
+                  <TableBody>
+                    {views.map((view, idx) => {
+                      const viewNamespace = view.namespace.join(".")
+                      return (
+                        <TableRow
+                          key={idx}
+                          className="cursor-pointer hover:bg-muted/50"
+                          onClick={() => handleViewClick(view.name)}
+                        >
+                          <TableCell>
+                            <div className="flex items-center gap-2">
+                              <TableIcon className="h-4 w-4 
text-muted-foreground" />
+                              <span className="font-medium">{view.name}</span>
+                            </div>
+                          </TableCell>
+                          <TableCell>
+                            <span className="text-muted-foreground 
text-sm">{viewNamespace}</span>
+                          </TableCell>
+                          <TableCell>
+                            <Button
+                              variant="ghost"
+                              size="sm"
+                              onClick={(e) => {
+                                e.stopPropagation()
+                                handleViewClick(view.name)
+                              }}
+                            >
+                              View Details
+                            </Button>
+                          </TableCell>
+                        </TableRow>
+                      )
+                    })}
+                  </TableBody>
+                </Table>
+              </div>
+            )}
+          </CardContent>
+        </Card>
+
       {/* Polaris Generic Tables Section */}
       <Card>
         <CardHeader>
@@ -599,6 +697,17 @@ export function NamespaceDetails() {
           </DialogFooter>
         </DialogContent>
       </Dialog>
+
+      {/* Create Iceberg View Modal */}
+      <CreateViewModal
+        open={createViewOpen}
+        onOpenChange={setCreateViewOpen}
+        catalogName={catalogName!}
+        namespace={namespaceArray}
+        onCreated={() => {
+          viewsQuery.refetch()
+        }}
+      />
     </div>
   )
 }
diff --git a/console/src/pages/ViewDetails.tsx 
b/console/src/pages/ViewDetails.tsx
new file mode 100644
index 0000000..f50f35a
--- /dev/null
+++ b/console/src/pages/ViewDetails.tsx
@@ -0,0 +1,350 @@
+/*
+ * 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.
+ */
+
+import { useState } from "react"
+import { useParams, useNavigate, Link } from "react-router-dom"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { ArrowLeft, RefreshCw, Eye, Trash2 } from "lucide-react"
+import { viewsApi } from "@/api/catalog/views"
+import { catalogsApi } from "@/api/management/catalogs"
+import { namespacesApi } from "@/api/catalog/namespaces"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 
"@/components/ui/card"
+import { SchemaViewer } from "@/components/table/SchemaViewer"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, 
DialogTitle } from "@/components/ui/dialog"
+import { toast } from "sonner"
+
+export function ViewDetails() {
+  const { catalogName, namespace: namespaceParam, viewName } = useParams<{
+    catalogName: string
+    namespace: string
+    viewName: string
+  }>()
+
+  const navigate = useNavigate()
+  const queryClient = useQueryClient()
+
+  const namespaceArray = namespaceParam?.split(".") || []
+
+  const catalogQuery = useQuery({
+    queryKey: ["catalog", catalogName],
+    queryFn: () => catalogsApi.get(catalogName!),
+    enabled: !!catalogName,
+  })
+
+  const namespaceQuery = useQuery({
+    queryKey: ["namespace", catalogName, namespaceArray],
+    queryFn: () => namespacesApi.get(catalogName!, namespaceArray),
+    enabled: !!catalogName && namespaceArray.length > 0,
+  })
+
+  const viewQuery = useQuery({
+    queryKey: ["view", catalogName, namespaceArray.join("."), viewName],
+    queryFn: () => viewsApi.get(catalogName!, namespaceArray, viewName!),
+    enabled: !!catalogName && namespaceArray.length > 0 && !!viewName,
+  })
+
+  // Modals
+  const [deleteOpen, setDeleteOpen] = useState(false)
+
+  // Delete mutation
+  const deleteMutation = useMutation({
+    mutationFn: () => viewsApi.delete(catalogName!, namespaceArray, viewName!),
+    onSuccess: () => {
+      toast.success("View deleted successfully")
+      queryClient.invalidateQueries({ queryKey: ["views", catalogName, 
namespaceArray] })
+      queryClient.invalidateQueries({ queryKey: ["namespace", catalogName, 
namespaceArray] })
+      
navigate(`/catalogs/${encodeURIComponent(catalogName!)}/namespaces/${encodeURIComponent(namespaceParam!)}`)
+    },
+    onError: (error: Error) => {
+      toast.error("Failed to delete view", {
+        description: error.message || "An error occurred",
+      })
+    },
+  })
+
+  if (!catalogName || !namespaceParam || !viewName) {
+    return <div>Catalog, namespace, and view name are required</div>
+  }
+
+  const nsPath = namespaceArray.join(".")
+  const refreshDisabled = viewQuery.isFetching || namespaceQuery.isFetching || 
catalogQuery.isFetching
+
+  const viewData = viewQuery.data
+
+  const handleDelete = () => {
+    deleteMutation.mutate()
+  }
+
+  const currentSchema = viewData?.metadata?.schemas?.find(
+    (s) => s["schema-id"] === viewData.metadata["current-version-id"]
+  ) || viewData?.metadata?.schemas?.[0]
+
+  const currentVersion = viewData?.metadata?.versions?.find(
+    (v) => v["version-id"] === viewData.metadata["current-version-id"]
+  )
+
+  const currentSql = currentVersion?.representations?.find(
+    (r) => r.type === "sql"
+  )
+
+  return (
+    <div className="p-6 md:p-8 space-y-6 overflow-y-auto">
+      {/* Header */}
+      <div className="flex items-start justify-between gap-4">
+        <div className="flex items-center gap-4">
+          <Button
+            variant="ghost"
+            size="icon"
+            onClick={() =>
+              navigate(
+                
`/catalogs/${encodeURIComponent(catalogName)}/namespaces/${encodeURIComponent(namespaceParam)}`
+              )
+            }
+          >
+            <ArrowLeft className="h-4 w-4" />
+          </Button>
+          <div className="space-y-1">
+            <div className="flex items-center gap-2">
+              <Eye className="h-6 w-6" />
+              <h1 className="text-2xl font-bold">{viewName}</h1>
+            </div>
+            <p className="text-muted-foreground">
+              <Link
+                to={`/catalogs/${encodeURIComponent(catalogName)}`}
+                className="underline-offset-2 hover:underline"
+              >
+                {catalogQuery.data?.name || catalogName}
+              </Link>
+              <span className="mx-1">/</span>
+              <Link
+                
to={`/catalogs/${encodeURIComponent(catalogName)}/namespaces/${encodeURIComponent(namespaceParam)}`}
+                className="underline-offset-2 hover:underline"
+              >
+                {nsPath}
+              </Link>
+              <span className="mx-1">/</span>
+              <span className="font-medium">{viewName}</span>
+            </p>
+          </div>
+        </div>
+        <div className="flex items-center gap-2">
+          <Button
+            variant="secondary"
+            onClick={() => {
+              viewQuery.refetch()
+              namespaceQuery.refetch()
+              catalogQuery.refetch()
+            }}
+            disabled={refreshDisabled}
+          >
+            <RefreshCw className="mr-2 h-4 w-4" />
+            Refresh
+          </Button>
+          <Button variant="destructive" onClick={() => setDeleteOpen(true)} 
disabled={!viewData}>
+            <Trash2 className="mr-2 h-4 w-4" /> Delete
+          </Button>
+        </div>
+      </div>
+
+      {viewQuery.isLoading ? (
+        <div>Loading view details...</div>
+      ) : viewQuery.error ? (
+        <div className="text-red-600">Error loading view: 
{viewQuery.error.message}</div>
+      ) : !viewData ? (
+        <div>View not found</div>
+      ) : (
+        <>
+          {/* View Information */}
+          <Card>
+            <CardHeader>
+              <CardTitle>View Information</CardTitle>
+              <CardDescription>Core metadata for this Iceberg 
view</CardDescription>
+            </CardHeader>
+            <CardContent>
+              <div className="space-y-4">
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
+                  <div>
+                    <label className="text-sm font-medium 
text-muted-foreground">UUID</label>
+                    <p className="mt-1 text-sm 
font-mono">{viewData.metadata["view-uuid"]}</p>
+                  </div>
+                  <div>
+                    <label className="text-sm font-medium 
text-muted-foreground">Format Version</label>
+                    <p className="mt-1 
text-sm">{viewData.metadata["format-version"]}</p>
+                  </div>
+                  <div>
+                    <label className="text-sm font-medium 
text-muted-foreground">Current Version ID</label>
+                    <p className="mt-1 
text-sm">{viewData.metadata["current-version-id"]}</p>
+                  </div>
+                  {currentSql && (
+                    <div>
+                      <label className="text-sm font-medium 
text-muted-foreground">SQL Dialect</label>
+                      <p className="mt-1 text-sm">{currentSql.dialect}</p>
+                    </div>
+                  )}
+                  <div className="md:col-span-2">
+                    <label className="text-sm font-medium 
text-muted-foreground">Location</label>
+                    <p className="mt-1 text-sm font-mono 
break-all">{viewData.metadata.location}</p>
+                  </div>
+                  {viewData["metadata-location"] && (
+                    <div className="md:col-span-2">
+                      <label className="text-sm font-medium 
text-muted-foreground">Metadata Location</label>
+                      <p className="mt-1 text-sm font-mono 
break-all">{viewData["metadata-location"]}</p>
+                    </div>
+                  )}
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+
+          {/* SQL Query */}
+          {currentSql && (
+            <Card>
+              <CardHeader>
+                <CardTitle>SQL Query</CardTitle>
+                <CardDescription>The SQL definition of this 
view</CardDescription>
+              </CardHeader>
+              <CardContent>
+                <pre className="bg-muted p-4 rounded-md overflow-x-auto 
text-sm font-mono whitespace-pre-wrap">
+                  {currentSql.sql}
+                </pre>
+              </CardContent>
+            </Card>
+          )}
+
+          {/* Schema */}
+          {currentSchema && (
+            <Card>
+              <CardHeader>
+                <CardTitle>Schema</CardTitle>
+                <CardDescription>Output schema of this view</CardDescription>
+              </CardHeader>
+              <CardContent>
+                <SchemaViewer schema={currentSchema} />
+              </CardContent>
+            </Card>
+          )}
+
+          {/* Properties */}
+          {viewData.metadata.properties && 
Object.keys(viewData.metadata.properties).length > 0 && (
+            <Card>
+              <CardHeader>
+                <CardTitle>Properties</CardTitle>
+                <CardDescription>View properties</CardDescription>
+              </CardHeader>
+              <CardContent>
+                <div className="border rounded-md overflow-hidden">
+                  <table className="w-full text-sm">
+                    <thead className="bg-muted/50">
+                      <tr>
+                        <th className="px-3 py-2 text-left 
font-medium">Key</th>
+                        <th className="px-3 py-2 text-left 
font-medium">Value</th>
+                      </tr>
+                    </thead>
+                    <tbody className="divide-y">
+                      {Object.entries(viewData.metadata.properties).map(([key, 
value]) => (
+                        <tr key={key} className="hover:bg-muted/50">
+                          <td className="px-3 py-2 font-mono 
text-xs">{key}</td>
+                          <td className="px-3 py-2 text-xs 
break-all">{String(value)}</td>
+                        </tr>
+                      ))}
+                    </tbody>
+                  </table>
+                </div>
+              </CardContent>
+            </Card>
+          )}
+
+          {/* Version History */}
+          {viewData.metadata.versions && viewData.metadata.versions.length > 0 
&& (
+            <Card>
+              <CardHeader>
+                <CardTitle>Version History</CardTitle>
+                <CardDescription>All versions of this view</CardDescription>
+              </CardHeader>
+              <CardContent>
+                <div className="space-y-3">
+                  {viewData.metadata.versions.map((version) => {
+                    const sqlRep = version.representations?.find((r) => r.type 
=== "sql")
+                    const isCurrent = version["version-id"] === 
viewData.metadata["current-version-id"]
+                    return (
+                      <div
+                        key={version["version-id"]}
+                        className={`border rounded-md p-3 ${isCurrent ? 
"border-primary bg-primary/5" : ""}`}
+                      >
+                        <div className="flex items-center justify-between 
mb-2">
+                          <span className="font-medium">
+                            Version {version["version-id"]}
+                            {isCurrent && (
+                              <span className="ml-2 text-xs 
text-primary">(current)</span>
+                            )}
+                          </span>
+                          <span className="text-xs text-muted-foreground">
+                            {new 
Date(version["timestamp-ms"]).toLocaleString()}
+                          </span>
+                        </div>
+                        {sqlRep && (
+                          <div className="text-xs">
+                            <span 
className="text-muted-foreground">Dialect:</span> {sqlRep.dialect}
+                          </div>
+                        )}
+                      </div>
+                    )
+                  })}
+                </div>
+              </CardContent>
+            </Card>
+          )}
+        </>
+      )}
+
+      {/* Delete Confirmation */}
+      {viewData && (
+        <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
+          <DialogContent>
+            <DialogHeader>
+              <DialogTitle>Delete view</DialogTitle>
+              <DialogDescription>
+                Are you sure you want to delete "{viewName}"? This action 
cannot be undone.
+              </DialogDescription>
+            </DialogHeader>
+            <DialogFooter>
+              <Button
+                variant="outline"
+                onClick={() => setDeleteOpen(false)}
+                disabled={deleteMutation.isPending}
+              >
+                Cancel
+              </Button>
+              <Button
+                variant="destructive"
+                onClick={handleDelete}
+                disabled={deleteMutation.isPending}
+              >
+                {deleteMutation.isPending ? "Deleting..." : "Delete"}
+              </Button>
+            </DialogFooter>
+          </DialogContent>
+        </Dialog>
+      )}
+    </div>
+  )
+}
+
+export default ViewDetails
diff --git a/console/src/types/api.ts b/console/src/types/api.ts
index 6d35596..ded04a9 100644
--- a/console/src/types/api.ts
+++ b/console/src/types/api.ts
@@ -253,6 +253,73 @@ export interface ListTablesResponse {
   nextPageToken?: string
 }
 
+// Views
+export interface ListViewsResponse {
+  identifiers: Array<{
+    namespace: string[]
+    name: string
+  }>
+  nextPageToken?: string
+}
+
+export interface ViewSchemaField {
+  id: number
+  name: string
+  type: string
+  required: boolean
+  doc?: string
+}
+
+export interface IcebergSchema {
+  type: "struct"
+  fields: ViewSchemaField[]
+  "schema-id"?: number
+  "identifier-field-ids"?: number[]
+}
+
+export interface SQLViewRepresentation {
+  type: "sql"
+  sql: string
+  dialect: string
+}
+
+export interface ViewVersion {
+  "version-id": number
+  "timestamp-ms": number
+  "schema-id": number
+  summary: Record<string, string>
+  representations: SQLViewRepresentation[]
+  "default-namespace": string[]
+}
+
+export interface CreateViewRequest {
+  name: string
+  location?: string
+  schema: IcebergSchema
+  "view-version": ViewVersion
+  properties: Record<string, string>
+}
+
+export interface ViewMetadata {
+  "view-uuid": string
+  "format-version": number
+  location: string
+  "current-version-id": number
+  versions: ViewVersion[]
+  "version-log": Array<{
+    "version-id": number
+    "timestamp-ms": number
+  }>
+  schemas: IcebergSchema[]
+  properties?: Record<string, string>
+}
+
+export interface LoadViewResult {
+  "metadata-location": string
+  metadata: ViewMetadata
+  config?: Record<string, string>
+}
+
 // LoadTableResult - response from GET 
/v1/{prefix}/namespaces/{namespace}/tables/{table}
 export interface LoadTableResult {
   "metadata-location"?: string | null

Reply via email to