import { FOREIGN_KEY_CASCADE_ACTION, safeSql } from '@supabase/pg-meta' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types' import type { ColumnField } from './SidePanelEditor.types' import { createTable } from './SidePanelEditor.utils' // Define mock functions at module level const mockExecuteSql = vi.fn() const mockGetTable = vi.fn() const mockSendEvent = vi.fn() const mockPrefetchEditorTablePage = vi.fn() const mockToastLoading = vi.fn() const mockToastSuccess = vi.fn() const mockToastError = vi.fn() const mockFetchQuery = vi.fn() // Setup mocks before imports vi.mock('@/data/query-client', () => ({ getQueryClient: () => ({ fetchQuery: mockFetchQuery, }), })) vi.mock('@/data/sql/execute-sql-query', () => ({ executeSql: (...args: unknown[]) => mockExecuteSql(...args), })) vi.mock('@/data/tables/table-retrieve-query', () => ({ getTable: (...args: unknown[]) => mockGetTable(...args), getTableQuery: (...args: unknown[]) => mockGetTable(...args), })) vi.mock('@/data/telemetry/send-event-mutation', () => ({ sendEvent: (...args: unknown[]) => mockSendEvent(...args), })) vi.mock('@/data/prefetchers/project.$ref.editor.$id', () => ({ prefetchEditorTablePage: (...args: unknown[]) => mockPrefetchEditorTablePage(...args), })) vi.mock('sonner', () => ({ toast: { loading: (...args: unknown[]) => mockToastLoading(...args), success: (...args: unknown[]) => mockToastSuccess(...args), error: (...args: unknown[]) => mockToastError(...args), }, })) // Mock SparkBar component used in toast vi.mock('@/components/ui/SparkBar', () => ({ default: () => null, })) // Helper to create a column field with defaults const createColumnField = (overrides: Partial = {}): ColumnField => ({ id: 'col-1', name: 'column', table: 'test_table', schema: 'public', format: 'text', check: null, comment: null, defaultValue: null, isNullable: true, isUnique: false, isArray: false, isIdentity: false, isPrimaryKey: false, isNewColumn: true, isEncrypted: false, ...overrides, }) describe('createTable', () => { const projectRef = 'test-project-ref' const connectionString = 'postgresql://localhost:5432/test' const toastId = 'test-toast-id' const basePayload = { name: 'test_table', schema: 'public', comment: 'A test table', } const mockTableResult = { id: 123, name: 'test_table', schema: 'public', comment: 'A test table', columns: [], primary_keys: [], relationships: [], } beforeEach(() => { vi.clearAllMocks() // Default mock implementations mockExecuteSql.mockResolvedValue({ result: [] }) mockGetTable.mockResolvedValue(mockTableResult) mockSendEvent.mockResolvedValue({}) mockPrefetchEditorTablePage.mockResolvedValue(undefined) mockFetchQuery.mockImplementation(({ queryFn }) => { if (queryFn) { return queryFn({ signal: new AbortController().signal }) } return Promise.resolve(mockTableResult) }) }) it('should create a basic table with no columns', async () => { const result = await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns: [], foreignKeyRelations: [], isRLSEnabled: false, }) expect(mockExecuteSql).toHaveBeenCalledTimes(1) expect(mockExecuteSql).toHaveBeenCalledWith( expect.objectContaining({ projectRef, connectionString, queryKey: ['table', 'create-with-columns'], }) ) // Should show loading toast expect(mockToastLoading).toHaveBeenCalledWith(`Creating table ${basePayload.name}...`, { id: toastId, }) // Should track table creation event expect(mockSendEvent).toHaveBeenCalledWith({ event: { action: 'table_created', properties: { has_generated_policies: false, method: 'table_editor', schema_name: 'public', table_name: 'test_table', }, groups: { project: projectRef, }, }, }) // Should prefetch the editor table page expect(mockPrefetchEditorTablePage).toHaveBeenCalledWith( expect.objectContaining({ projectRef, connectionString, id: mockTableResult.id, }) ) expect(result).toStrictEqual({ failedPolicies: [], table: mockTableResult, }) }) it('should create a table with RLS enabled', async () => { await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns: [], foreignKeyRelations: [], isRLSEnabled: true, }) const sqlCall = mockExecuteSql.mock.calls[0][0] expect(sqlCall.sql).toContain('ENABLE ROW LEVEL SECURITY') expect(mockSendEvent).toHaveBeenCalledWith({ event: { action: 'table_rls_enabled', properties: { method: 'table_editor', schema_name: 'public', table_name: 'test_table', }, groups: { project: projectRef, }, }, }) }) it('should create a table with columns', async () => { const columns: ColumnField[] = [ createColumnField({ id: 'col-1', name: 'id', format: 'int8', isNullable: false, isIdentity: true, isPrimaryKey: true, }), createColumnField({ id: 'col-2', name: 'name', format: 'text', comment: 'User name', }), ] await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns, foreignKeyRelations: [], isRLSEnabled: false, }) const sqlCall = mockExecuteSql.mock.calls[0][0] expect(sqlCall.sql).toContain('ALTER TABLE') expect(sqlCall.sql).toContain('ADD COLUMN') expect(sqlCall.sql).toContain('ADD PRIMARY KEY') expect(sqlCall.sql).toContain('id') }) it('should create a table with composite primary key', async () => { const columns: ColumnField[] = [ createColumnField({ id: 'col-1', name: 'user_id', format: 'int8', isNullable: false, isPrimaryKey: true, }), createColumnField({ id: 'col-2', name: 'order_id', format: 'int8', isNullable: false, isPrimaryKey: true, }), ] await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns, foreignKeyRelations: [], isRLSEnabled: false, }) const sqlCall = mockExecuteSql.mock.calls[0][0] expect(sqlCall.sql).toContain('ADD PRIMARY KEY') expect(sqlCall.sql).toContain('user_id') expect(sqlCall.sql).toContain('order_id') }) it('should create a table with foreign key relations', async () => { const columns: ColumnField[] = [ createColumnField({ id: 'col-1', name: 'id', format: 'int8', isNullable: false, isIdentity: true, isPrimaryKey: true, }), createColumnField({ id: 'col-2', name: 'user_id', format: 'int8', isNullable: false, }), ] const foreignKeyRelations: ForeignKey[] = [ { schema: 'public', table: 'users', columns: [{ source: 'user_id', target: 'id' }], deletionAction: FOREIGN_KEY_CASCADE_ACTION.CASCADE, updateAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, }, ] await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns, foreignKeyRelations, isRLSEnabled: false, }) const sqlCall = mockExecuteSql.mock.calls[0][0] expect(sqlCall.sql).toContain('ADD FOREIGN KEY') expect(sqlCall.sql).toContain('REFERENCES') expect(sqlCall.sql).toContain('users') expect(sqlCall.sql).toContain('ON DELETE CASCADE') }) it('should include organization slug in telemetry when provided', async () => { const organizationSlug = 'test-org' await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns: [], foreignKeyRelations: [], isRLSEnabled: true, organizationSlug, }) expect(mockSendEvent).toHaveBeenCalledWith({ event: expect.objectContaining({ action: 'table_created', groups: { project: projectRef, organization: organizationSlug, }, }), }) expect(mockSendEvent).toHaveBeenCalledWith({ event: expect.objectContaining({ action: 'table_rls_enabled', groups: { project: projectRef, organization: organizationSlug, }, }), }) }) it('should handle telemetry errors gracefully', async () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) mockSendEvent.mockRejectedValue(new Error('Telemetry failed')) const result = await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns: [], foreignKeyRelations: [], isRLSEnabled: false, }) expect(result).toStrictEqual({ failedPolicies: [], table: mockTableResult, }) expect(consoleErrorSpy).toHaveBeenCalledWith( 'Failed to track table creation event:', expect.any(Error) ) consoleErrorSpy.mockRestore() }) it('should create a table with nullable connectionString', async () => { await createTable({ projectRef, connectionString: null, toastId, payload: basePayload, columns: [], foreignKeyRelations: [], isRLSEnabled: false, }) expect(mockExecuteSql).toHaveBeenCalledWith( expect.objectContaining({ projectRef, connectionString: null, }) ) }) it('should create table with column having default value', async () => { const columns: ColumnField[] = [ createColumnField({ name: 'status', defaultValue: "'pending'", isNullable: false, }), ] await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns, foreignKeyRelations: [], isRLSEnabled: false, }) expect(mockExecuteSql).toHaveBeenCalledTimes(1) }) it('should create table with unique column', async () => { const columns: ColumnField[] = [ createColumnField({ name: 'email', isNullable: false, isUnique: true, }), ] await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns, foreignKeyRelations: [], isRLSEnabled: false, }) expect(mockExecuteSql).toHaveBeenCalledTimes(1) }) it('should create table with array column', async () => { const columns: ColumnField[] = [ createColumnField({ name: 'tags', isArray: true, }), ] await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns, foreignKeyRelations: [], isRLSEnabled: false, }) expect(mockExecuteSql).toHaveBeenCalledTimes(1) }) it('should create table with check constraint', async () => { const columns: ColumnField[] = [ createColumnField({ name: 'age', format: 'int4', check: safeSql`age >= 0`, isNullable: false, }), ] await createTable({ projectRef, connectionString, toastId, payload: basePayload, columns, foreignKeyRelations: [], isRLSEnabled: false, }) expect(mockExecuteSql).toHaveBeenCalledTimes(1) }) it('should propagate SQL execution errors', async () => { mockExecuteSql.mockRejectedValue(new Error('SQL execution failed')) await expect( createTable({ projectRef, connectionString, toastId, payload: basePayload, columns: [], foreignKeyRelations: [], isRLSEnabled: false, }) ).rejects.toThrow('SQL execution failed') }) it('should create table in non-public schema', async () => { const customSchemaPayload = { name: 'custom_table', schema: 'private', comment: 'A private table', } await createTable({ projectRef, connectionString, toastId, payload: customSchemaPayload, columns: [], foreignKeyRelations: [], isRLSEnabled: false, }) const sqlCall = mockExecuteSql.mock.calls[0][0] expect(sqlCall.sql).toMatch(/private\.custom_table|"private"\."custom_table"/) expect(mockSendEvent).toHaveBeenCalledWith({ event: expect.objectContaining({ properties: expect.objectContaining({ schema_name: 'private', }), }), }) }) })