Files
Ali Waseem c39bb96d74 feat: Context view actions for views and material views (#46383)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Right now material views and views don't have any options on the context
menu, they only have a copy name. This adds copy schema, export CSV,
export SQL and delete table to that list

Added E2E tests to cover the use cases

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Delete views and materialized views via confirmation dialogs with
optional cascade
  * Copy SQL definitions for views and materialized views
* Export views and materialized views as CSV and SQL from the entity
menu
* Confirmation modals now show dependency warnings and cascade toggle
consistently

* **Tests**
* End-to-end tests covering copy, export, and delete flows for views and
materialized views in the table editor

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46383?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-28 16:19:27 +00:00

110 lines
3.4 KiB
TypeScript

import { query } from './client.js'
/**
* Check if a table exists in the specified schema.
*
* @param schema - The schema name (e.g., 'public')
* @param tableName - The table name to check
* @returns true if the table exists, false otherwise
*/
export async function tableExists(schema: string, tableName: string): Promise<boolean> {
const result = await query<{ exists: boolean }>(
`SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = $1 AND table_name = $2
) as exists`,
[schema, tableName]
)
return result[0]?.exists ?? false
}
/**
* Create a table with a single text column and optionally insert initial rows.
*
* @param tableName - The table name to create
* @param columnName - The name of the text column
* @param initialData - Optional array of rows to insert (each row is a key-value map)
*/
export async function createTable(
tableName: string,
columnName: string,
initialData?: Array<Record<string, string>>
) {
await query(
`CREATE TABLE IF NOT EXISTS ${tableName} (
id bigint generated by default as identity primary key,
created_at timestamp with time zone null default now(),
${columnName} text
)`
)
if (initialData && initialData.length > 0) {
const placeholders = initialData.map((_, i) => `($${i + 1})`).join(', ')
const values = initialData.map((row) => row[columnName])
await query(`INSERT INTO ${tableName} (${columnName}) VALUES ${placeholders}`, values)
}
}
/**
* Create a table with a single text column with RLS enabled (as if created through UI) and optionally insert initial rows.
*
* @param tableName - The table name to create
* @param columnName - The name of the text column
* @param initialData - Optional array of rows to insert (each row is a key-value map)
*/
export async function createTableWithRLS(
tableName: string,
columnName: string,
initialData?: Array<Record<string, string>>
) {
await createTable(tableName, columnName, initialData)
await query(`alter table public.${tableName} enable row level security;`)
}
/**
* Drop a table if it exists.
*
* @param tableName - The table name to drop
*/
export async function dropTable(tableName: string) {
await query(`DROP TABLE IF EXISTS ${tableName} CASCADE`)
}
/**
* Create a view in the public schema. Assumes the underlying table already exists.
*
* @param viewName - The view name to create
* @param selectSql - The SELECT statement that defines the view (without trailing semicolon)
*/
export async function createView(viewName: string, selectSql: string) {
await query(`CREATE OR REPLACE VIEW public.${viewName} AS ${selectSql}`)
}
/**
* Drop a view if it exists.
*
* @param viewName - The view name to drop
*/
export async function dropView(viewName: string) {
await query(`DROP VIEW IF EXISTS public.${viewName} CASCADE`)
}
/**
* Create a materialized view in the public schema.
*
* @param viewName - The materialized view name to create
* @param selectSql - The SELECT statement that defines the view (without trailing semicolon)
*/
export async function createMaterializedView(viewName: string, selectSql: string) {
await query(`CREATE MATERIALIZED VIEW IF NOT EXISTS public.${viewName} AS ${selectSql}`)
}
/**
* Drop a materialized view if it exists.
*
* @param viewName - The materialized view name to drop
*/
export async function dropMaterializedView(viewName: string) {
await query(`DROP MATERIALIZED VIEW IF EXISTS public.${viewName} CASCADE`)
}